feat: Home resumes by recency, and its one control follows what is waiting - #560
Merged
Conversation
… on it `annotation_job_asset` gains a nullable `touched_at`, written inside `UnitOfWork.set_asset_progress`'s guarded UPDATE so the stamp is as atomic as the transition it records. Migration 8 adds it without a backfill: the moments it holds were never recorded anywhere, so every existing row starts NULL and means "nobody has worked this frame since the column existed". `SummaryService` ranks open batches on it. A batch somebody has worked outranks one nobody has, the later touch wins between two that have been, and batches whose rows are all NULL keep the progress ranking that shipped before — which is every batch in a workspace created before migration 8. `_refresh_progress` now writes even when the derived progress does not move, so an edit to an already-annotated frame stamps. `JobService.mark` keeps its no-op short-circuit: a client re-sending a value the asset already holds is a retry rather than somebody working the frame. The resume target also gains a `kind` — annotate, review or open — resolved here because the order between the three is a decision rather than a fact the rest of the projection restates.
The resume card renders the kind the wire declares rather than working it out: **Continue annotating**, **Review annotations** or **Open batch**, the first two opening the editor and the third the gallery. A review-pending frame opens read-only with the review actions on it, so there is no second screen to send anybody to — and the count line follows the label, since how much of the batch is labeled is not the number anybody came for once the labeling is done.
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.
Two flags left open when Home shipped (#552, merged as #556), settled together because they are
one mechanism: the review CTA is ranked by the same recency the column restores.
The column
annotation_job_assetgains a nullabletouched_at, and it is the only timestamp in the schemathat dates a person's work rather than a record's creation.
It cost one line at the write, because there was exactly one write to change.
UnitOfWork.set_asset_progressis the narrow compare-and-set introduced when concurrent progresswrites were being lost (#302); every progress transition goes through it, and
_job_write_childrenwrites a
progressvalue only on insert, so no other path can move one. The stamp rides in thesame guarded
UPDATE, which makes it as atomic as the transition it records and unable to driftfrom it. A write that loses its race stamps nothing, because losing a race is not work.
No backfill, and migration 8 says why. The moments the column holds were never recorded
anywhere — not in another row, not in a derived field, not on disk — so a workspace's history of
who-worked-when begins here. Every existing row is NULL, which reads as nobody has touched this
frame since the column existed.
That is what the ranking's second population is for. A batch with a stamp outranks every batch
without one and the later touch wins between two that have; batches whose rows are all NULL are
ranked among themselves by the rule that shipped before — furthest through first, ties to the
later-created batch. A workspace opened straight after upgrading therefore behaves exactly as it
did, and converges to real recency as soon as anybody works in it.
One deliberate limit on where the stamp reaches.
_refresh_progressnow writes even when thederived progress does not move, so drawing a second box on an already-
annotatedframe touches itsbatch — that is most of what annotating actually is, and a stamp written only on transitions would
miss it.
JobService.markkeeps its no-op short-circuit: a client re-sending a value the assetalready holds is a retry rather than somebody working the frame, and that early return is a
documented part of what
markmeans.The CTA priority
Home shows exactly one filled button in every state. It now resolves in the order the workspace
actually needs:
annotatereviewopenReview opens the annotator, and there is no second screen to build.
docs/ui.mdalready tookthis position: "There is one screen, not two. Which controls appear is the frame's state, so the
annotator and the reviewer are the same page wearing what it is looking at." A
review_pendingframe opens read-only with Accept and Return to annotator on it, gated on its own declared
actions — so the review target has the same shape as the annotate target, a job and an asset, and
reaches it through the same deep link. The gallery's In review filter segment is component state
rather than a URL parameter and is left alone.
Open batch survives, as a fourth outcome rather than a third state. The decision comment names
three; the shipped card already flips to Open batch when a batch is settled throughout, and an
open batch nobody has completed is a better answer to "what do I do next?" than starting another
project. Recorded on #559 before implementing.
The kernel resolves which applies.
ResumeTarget.kindis new, and it is the field to readfirst: it says whether
next_asset_idis a frame to label, one to review, or absent. That is ajudgment about what somebody should do next rather than a fact the rest of the response restates,
so a browser working it out again would keep a second copy of a rule that can drift. It is
deliberately unlike the first-run state, which is still not a field because
totals.projectsalready answers it — the distinction is written into the enum and into
docs/ui.md, since the twowould otherwise read as contradictory.
review_pendingis new beside it, and is populated in every kind rather than only inreview: abatch can hold frames for review and unlabeled frames at once, and the count is more interesting in
that case, not less.
Verification
Every new rule was mutation-verified after committing the implementation: one unconditional step
per case, anchor asserted present exactly once before and replaced after, revert by recorded diff,
clean tree checked between cases. Sixteen mutations, sixteen reddened a named test, sixteen
reverts landed.
test_the_batch_you_worked_last_winstest_an_edit_that_moves_no_progress_still_counts_as_working_the_batchtest_a_split_batch_is_as_recent_as_its_newest_job..._wins_across_projects_tootest_a_batch_somebody_has_worked_beats_one_nobody_hastest_the_batch_you_are_furthest_through_wins_where_nobody_has_been>=→>)test_a_tie_goes_to_the_batch_created_latertest_review_outranks_a_batch_with_neithertest_labeling_in_one_batch_outranks_review_in_a_more_recent_oneOne came back green and it was a real hole. Taking the oldest of a batch's jobs instead of
the newest changed nothing, because every fixture in the file made single-job batches and a batch
with one job cannot tell
maxfrommin. A partition is what reaches it, sotest_a_split_batch_is_as_recent_as_its_newest_jobcuts a batch into one job per asset and workstwo of them either side of working a different batch. The mutation reddens it now.
Writing that test also caught my own first version of it: the split batch became fully annotated,
so
kinddecided the ranking before recency was ever consulted and it failed against correct code.It carries a third asset now, and a comment saying why.
The gate, staged
Run in stages because the harness kills at ~10 minutes. Exit codes verbatim:
openapi.jsonre-exported after the fact leaves no diff, so the committed spec matches the app.ui-core vitest, red on the merge-base too — #555
Reproduced here, by me, in this environment, on unmodified
mainat the merge-based6777d6:The same count, in the same files —
inference.test.tsx,schemaDraft.test.tsx,screens.test.tsx,addClassProvenance.test.tsx— and every failure isError: Test timed out in 5000ms.home.test.tsxpasses on every run, 21 of 21, and it is theonly ui-core file this branch touches. CI's
frontendjob is green. Tracked as #555.The local e2e suite did not produce a usable verdict, and CI is the arbiter
Declared rather than silent.
annotator + app e2e (chromium)failed locally on this machineand the failures are machine saturation, not this diff. Two runs, and the failing set is different
each time:
Every failure is
Test timeout of 20000ms exceeded, and the second run's set includes a copy/pastescenario this diff cannot reach. The load average on this laptop went from 45 to 394 across the
runs, from a second unrelated session; nothing this task spawned, and no orphaned process of mine
(checked by
PPID == 1). Run 2 was stopped for that reason rather than finished, and its processeswere confirmed dead.
The two review scenarios were checked rather than assumed, because they are the ones this
change could plausibly have broken. Run alone at two workers:
This is the sibling of #511 and #550 in this suite, and of #555 one suite over. CI on clean runners
is the arbiter: all fourteen required checks are green, including
annotator e2e (chromium)andbrowser cycle (chromium)— the real-server run the protocol prizes most.Driven in a browser, on a real server
The suite above being unusable is exactly when this matters, so all four states were walked by hand
against
visionset serverover a seeded workspace with two projects: one batch part-way throughlabeling, one fully labeled with a frame sent back for review.
annotatewins over a more recently worked review batch. The part-way batch was workedfirst and still won, which is the tier doing its job.
reviewon the other project — card readingBatch 3 · 1 waiting on reviewwith Review annotations./jobs/{job}?asset={frame}with thein reviewchip, the read-onlybanner ("This frame is waiting on a review — return it to the annotator to change its labels"),
Accept offered, and Skip and Delete disabled. This is the whole argument for not building a
second screen, confirmed rather than asserted.
kind: openand an empty attention list — and Highway pilot then outrankedthe other batch on recency, because accepting stamped it. The new column working live.
Found, not fixed
domain/task.py:107and:123still say "M1 has no review surface" and "there is no reviewsurface yet", and
docs/jobs.md:48,215repeats it, whiledocs/ui.mddocuments the annotator'sthree review moves as shipped. Stale prose rather than stale behaviour; this change relies on the
shipped moves and does not alter them.
_DECLARED_TAILSintests/kernel/test_migrations.pyhas no entry forinference_connection,whose
model_familyarrived byALTERin migration 7. Adding this change's own entry made thegap visible.
Flags for Armando
touched_atdates work, not the batch: nothingrecords when a batch was created or when it changed state, so the tie-break within the untouched
population is still insertion order, and the oldest batch nobody has started remains
underivable. The project header's Annotate control hit the same wall from the other side
(Project header Annotate CTA silently picks the first active batch when several are in annotation #544, shipped in fix(ui): the project header asks which batch when several are open for annotation #545).
batch.created_at/state_changed_atis a domain-model decision withwider consequences than a progress stamp and wants its own issue; it is recorded as out of scope
on Restore true-recency resume via annotation_job_asset.touched_at #558 rather than taken here.
markto a state the asset already holds, per thedecision above. If a surface ever wants "somebody looked at this batch" rather than "somebody
changed something in it", that is the case to revisit.
review_pendingis reachable only through the annotator's own controls. There is no reviewqueue to send anybody to and Home does not invent one: the CTA lands on a frame, not on a
filtered list. If reviewing several frames in a row becomes the common path, making the gallery's
In review segment addressable (
?segment=review) is the small change that would support it.Closes #558
Closes #559