Repository navigation
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
Conversation
… 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.
The contract this PR dropped, item by itemThe package used to assert a number that a comment stated: the guard read the number out of the The obligations below are about that contract. They are withdrawn, not left open: an obligation The identifiers are the ones these obligations were given in the review of this change; nothing else
What a reader of this pull request should take from the table. The dropped contract was a check |
MyPrototypeWhat
left a comment
There was a problem hiding this comment.
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)
- Parsed-rows cache can go over its byte budget.
miniapp/node/server.mjs:3788evicts only whileentries.size > 1, so a single large current session stays cached even pastPARSED_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. - 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.
- Data-dir guard when
pluginRootis missing.assertDataDirOutsidePlugin(server.mjs:4176-4185) passes''toisUnderDirectory(server.mjs:1026), which resolves to the process cwd. If the runtime ever omitspluginRoot, a validdataDirunder the cwd would be refused. Skipping the check, or failing with a clear message, whenpluginRootis empty would be safer. - New reads of session-store data. The plugin's existing sources on main stay the same. What this PR adds on top:
manifest.jsonnow 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 selectspurposeandworkspace_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. - 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. around1660-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 atserver.mjs:1296-1322. Paths outside the sessions root are rejected atserver.mjs:1679. - Refusing to write into the package dir:
server.mjs:4176-4191, called first instart(). Derived files are written only undercontext.dataDir/trajectory-artifacts(miniapp/node/search-segments.mjs:430-449). - Cache bounds: session cache by count (
SESSION_CACHE_ENTRIES,server.mjs:91, enforced at4338), 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.
|
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.
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
wire names that id. A path counts as a session's path only when the directory's name and its
manifest agree.
moves backwards.
released. Derived artifacts go under the runtime's own data directory, and the service refuses a
data directory that resolves inside the package.
budget; a position read says which record of the body it means, and a cached answer says which
answer it is holding.
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:
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.
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.
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.
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.
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 ofminiapp/client/index.html(the picker, the ledger). The two caches are pinned bytests/verify-parsed-rows.mjs(95 assertions),tests/mutate-parsed-rows.py(36 cases, eachcaught by name) and
tests/mutate-parsed-rows-owner-escapes.py(6 cases, each caught by name).Known weaknesses:
limit constant is not something the suite objects to.
limit bounds the bytes, not the number of conversations.
Disclosures
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.
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.
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.
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.
each item names what was dropped, why, and what covers the behaviour now.
session lookup. The id a directory's
manifest.jsoncarries is now checked against the expectedsession id before the directory is read (
server.mjs:1218-1221,1294-1320,1674-1698); thepicker's sub-agent layer is derived from the parent link the lookup already selected
(
server.mjs:1860-1876); and the lookup now also selectspurposeandworkspace_dir(
server.mjs:1484-1485), two columns it did not select before. Reading the host'ssession 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.