feat(inference): the Inference screen watches a download instead of owning one - #494
Merged
Conversation
…wning one The screen followed a job id it kept in component state, set from the `202` it had itself received. So only the mount that pressed the button could see a transfer: a reload, a second tab, or walking to another screen and back all produced `Not set up` beside a download that was still running. And what it showed was `1 of 5` in a microline — no bar, and no sentence for any state the job could be in. Everything about a transfer now comes off `ConnectionOut.download`. The list re-reads itself while any row reports a live one and stops the moment none does, so recovery on return, reload or a fresh tab is a property of the shape rather than a feature. Nothing the browser does reaches the job — it runs in a worker the server owns — so leaving the screen stops the poll and nothing else. A determinate bar with prose beside it, in human sizes: queued names the queue, transferring reads `400.0 MB of 1.6 GB · 25%`, and the phase after the last byte — reading what arrived, recording the connection ready — is named rather than left as a bar sitting full. A total that could not be read gets the sentence and no bar, because `Progress` renders an indeterminate value as an empty track and an empty track reads as nought. DESIGN.md's second brand site is restated at the primitive rather than at one of its callers: the recorded reason argues about what a progress bar is, and a rule phrased as *the ingest bar* would make every later bar look like a third site. The invariant stays checkable by the same grep, and its count is corrected — four lines match, three of them usages. The integrity check keeps the job id the download gave up. It is a different question with its own vocabulary and it is not on the connection's wire model, so there is nothing to read it off; #492 records that it still has the coupling.
`tests/scripts/e2e_discipline` forbids a browser spec from waiting on a clock, and it is right to: an assertion that nothing happens over an interval is exactly the shape that turns a busy runner into a red suite. Two of the new browser tests were making that claim with `waitForTimeout`. It moves to `inference.test.tsx`, where the request log is the state and jsdom's scheduler is not the thing under test. The browser spec keeps what only a browser can say: a page that never started a transfer showing it, a reload not losing it, and a bar that moves because a timer fired rather than because anything was clicked.
This was referenced Aug 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The Inference screen half of #492, on top of the wire PR 1 landed. Frontend and
DESIGN.mdonly — no kernel, no wire, no generated artifacts.Closes #492.
What was wrong
The screen followed a job id it kept in component state, set from the
202ithad itself received. So only the mount that pressed the button could see a
transfer: a reload, a second tab, or walking to another screen and back all
produced
Not set upbeside a download that was still running, next to aDownload weightsbutton that looked as though nothing had been done.What it showed was
1 of 5in a microline. No bar, and no sentence for any statethe job could be in.
What it does now
Everything comes off
ConnectionOut.download.useDownloadRunholds no jobid; it reads the row. Recovery is then a property of the shape rather than a
feature anybody wrote — a page that arrives mid-transfer sees it on its first
fetch, because the connection it was going to list says so.
The list polls itself, and only while something is moving.
useConnectionstakes a
refetchIntervalthat is a function of the answer: two seconds while anyrow reports a live transfer,
falseotherwise. Nothing the browser does reachesthe job — it runs in a worker the server owns — so navigating away stops the poll
and nothing else.
It deliberately does not reuse
usePollingQuery, which keeps asking when ithas no data. That is the right rule for a job somebody is waiting on and the
wrong one here: this list is also the annotator's read, and a broken endpoint
would be polled forever behind a screen that never asked for one.
A determinate bar with prose beside it, both and never one — a bar cannot say
queued, and prose alone makes somebody read a number every two seconds to find
out whether anything is moving.
queuedrunning400.0 MB of 1.6 GB · 25%, determinate.running, every byte insucceededReadybadge is the success treatment.failedallowed_actions(principle 9).Sizes, not a bare percentage: 38% of an unstated amount answers neither how
much longer nor how much disk. Tabular figures per
DESIGN.md's Numbers rule,because the number changes every two seconds and the words after it must not move
under a reader's eye.
DESIGN.mdThe brand's second site is restated from the ingest progress bar's fill to
the
Progressprimitive's fill. The recorded rationale — a progress bar isthe one piece of chrome a person watches rather than reads — already argues at
the level of the component, so a rule phrased as one of its callers would make
every later bar look like the third site the list forbids. The invariant stays
checkable by the same
git grep, and adding a caller adds no hit, because thefill lives inside the primitive. The
--color-brandcomment instyles.csscarried the same stale phrasing and moves with it.
The expected-hits note was already wrong and is corrected here. The grep
returns four lines, not three: the fourth is that
styles.csscomment, whichstates the rule rather than applying it. Three usages is the invariant. This is
adjacent to the sentence being rewritten, so leaving a count a reader can falsify
in one command seemed worse than fixing it.
Decisions worth arguing with
A total that could not be read gets prose and no bar, where the issue asked
for an indeterminate treatment.
Progressrenders an indeterminate value as anempty track, which reads as 0% — a lie in the one case where the truth is this
is going, and nobody can say how far. Giving the primitive an indeterminate
animation is a design-system change and not this screen's to make; the row says
700.0 MB so far — the published size could not be read.The same reasoninggives the settling phase a full bar plus its own sentence rather than an
indeterminate one: every byte really is here.
bytes()became locale-aware — oneIntl.NumberFormat, built once at modulescope.
DESIGN.md's Numbers rule asks for one shared helper and this screen nowformats sizes in four places. A whole number of bytes stays whole (
512 B, not512.0 B).The integrity check keeps its job id. It is a different question with its own
vocabulary and it is not on the connection's wire model, so there is nothing to
read it off — it therefore still has the coupling the download shed, and a reload
still loses a check in flight. Recorded on #492 rather than fixed here; widening
the wire for it is a second design decision, not a consequence of this one.
Test plan
Mutation-verified, three mutations, each reverted by its recorded diff
(
git apply -R) with the anchor asserted unique before and present after:refetchIntervaldeletedfollows a transfer to its end with no reload and no clicklivesourced frommutation.isPendinginstead of the wireNew in
inference.test.tsx: a transfer nobody on the page started; the queuenamed rather than a bar drawn; the settling phase; no bar without a total; no
progress for a connection never downloaded; the poll following a transfer to its
end and stopping; a list nothing is moving in never re-read; and a failure read
off the row with nothing clicked.
New in
frontend/app/e2e/inference.spec.ts— the claims that need a browser:page.reload(). This is the shipped bug's exact shape, and there is no jsdomequivalent of throwing the whole application away — React tree, query cache,
every closure that might have held a job id — and seeing what comes back.
navigation.
was watching still saying why.
One thing moved on the repo's say-so. Two of those browser tests originally
asserted "and it stops asking" with
page.waitForTimeout, andtests/scripts/e2e_disciplineforbids a spec from waiting on a clock. It isright to: an assertion that nothing happens over an interval is the shape that
turns a busy runner red. The counting claim moved to
inference.test.tsx, wherethe request log is the state and jsdom's scheduler is not under test; the second
commit is that change alone.
Found, not fixed
useConnectionsnow polls for every caller, including the annotator'ssuggest panel, when a download is live in that workspace. It is correct — the
panel wants to notice a connection becoming ready — but it is a behaviour
change for a screen that did not ask for one, and it is worth knowing about
before somebody debugs a request every two seconds behind the editor.
Local gate
Full run, staged against this box's ~10-minute command ceiling, pytest split by
directory derived from
ls tests/at run time. Every stage's exit code:pytest tests/architecture0pytest tests/cli0pytest tests/examples0pytest tests/fixtures0pytest tests/formats0pytest tests/inference0pytest tests/jobs0pytest tests/kernel0pytest tests/mcp0pytest tests/packaging0pytest tests/scripts5pytest tests/server0pytest tests/test_versioning.py0ruff check .0ruff format --check .0mypy src/visionset/kernel0lint-imports0— 4 contracts keptcheck.sh frontend generated0check.sh browser0— 243 e2e passed, 1 cycle passedtests/scriptsexits 5 by design: nothing pytest-shaped lives there, it isnode --testand runs undercheck.sh generated. That group has a real stake inthis diff —
e2e_discipline.test.mjsis what sent the two clock waits back.cf. #434, #454, #470, #471, #493.