Home's "Continue where you left off" card was specified as the most recently active batch and
did not ship that way. Building the workspace dashboard (#552) turned up the reason: there is no
timestamp column on batch, on annotation, or on annotation_job_asset. Nothing in the
storage format records when a batch was created, when it changed state, when a label was drawn, or
when an asset's progress last moved. Recency had no source, so the card shipped ranked by
progress instead — the batch furthest through that still has an unannotated frame — and
DESIGN.md, docs/api.md and the kernel model all state that substitution rather than hiding it.
The accepted limit was written down at the time: two batches part-way through resolve to the
further-along one rather than to the one somebody actually touched last. That is the defect this
issue closes.
The fix is one nullable column
annotation_job_asset gains touched_at, nullable, holding an ISO-8601 UTC string like every
other timestamp in the schema.
It is cheap because the write already exists and there is exactly one of it.
UnitOfWork.set_asset_progress is the narrow compare-and-set introduced when concurrent progress
writes were being lost (#302); every progress transition in the product goes through it, and
_job_write_children writes a progress value only on insert, so no other path can move one. The
stamp rides inside that same guarded UPDATE, which makes it as atomic as the transition it
records and unable to drift from it.
Nothing else changes shape. GET /home keeps its fields, the resume card keeps its layout, and no
surface gains a control.
What a NULL means, and the fallback
No backfill. A row that predates the column stays NULL, which reads as nobody has worked this
frame since the column existed — the same posture annotation_schema.provenance and
inference_connection.model_family already take for values that could not honestly be
reconstructed.
So the ranking becomes two populations rather than one:
- Any batch with a
touched_at outranks every batch without one, and among those the latest touch
wins.
- Batches whose rows are all NULL are ranked among themselves by the progress rule that ships
today, ties going to the later-created batch.
A workspace opened straight after upgrading therefore behaves exactly as it does now, and
converges to true recency as soon as somebody works in it.
Out of scope, and needs a decision
Whether batch itself gains created_at and state_changed_at is not decided here. With
touched_at in place the untouched population is still ordered by insertion, and a batch that
nobody has annotated in still has no age. The project header's Annotate control hit the same wall
from the other side — BatchOut carries no timestamp, so "the most recent batch" had no wire
source and that work asked the operator which batch instead (#544, shipped in #545). Adding
lifecycle timestamps to a batch is a domain-model decision with more consequences than a progress
stamp has, and it wants its own issue.
Home's "Continue where you left off" card was specified as the most recently active batch and
did not ship that way. Building the workspace dashboard (#552) turned up the reason: there is no
timestamp column on
batch, onannotation, or onannotation_job_asset. Nothing in thestorage format records when a batch was created, when it changed state, when a label was drawn, or
when an asset's progress last moved. Recency had no source, so the card shipped ranked by
progress instead — the batch furthest through that still has an unannotated frame — and
DESIGN.md,docs/api.mdand the kernel model all state that substitution rather than hiding it.The accepted limit was written down at the time: two batches part-way through resolve to the
further-along one rather than to the one somebody actually touched last. That is the defect this
issue closes.
The fix is one nullable column
annotation_job_assetgainstouched_at, nullable, holding an ISO-8601 UTC string like everyother timestamp in the schema.
It is cheap because the write already exists and there is exactly one of it.
UnitOfWork.set_asset_progressis the narrow compare-and-set introduced when concurrent progresswrites were being lost (#302); every progress transition in the product goes through it, and
_job_write_childrenwrites aprogressvalue only on insert, so no other path can move one. Thestamp rides inside that same guarded
UPDATE, which makes it as atomic as the transition itrecords and unable to drift from it.
Nothing else changes shape.
GET /homekeeps its fields, the resume card keeps its layout, and nosurface gains a control.
What a NULL means, and the fallback
No backfill. A row that predates the column stays NULL, which reads as nobody has worked this
frame since the column existed — the same posture
annotation_schema.provenanceandinference_connection.model_familyalready take for values that could not honestly bereconstructed.
So the ranking becomes two populations rather than one:
touched_atoutranks every batch without one, and among those the latest touchwins.
today, ties going to the later-created batch.
A workspace opened straight after upgrading therefore behaves exactly as it does now, and
converges to true recency as soon as somebody works in it.
Out of scope, and needs a decision
Whether
batchitself gainscreated_atandstate_changed_atis not decided here. Withtouched_atin place the untouched population is still ordered by insertion, and a batch thatnobody has annotated in still has no age. The project header's Annotate control hit the same wall
from the other side —
BatchOutcarries no timestamp, so "the most recent batch" had no wiresource and that work asked the operator which batch instead (#544, shipped in #545). Adding
lifecycle timestamps to a batch is a domain-model decision with more consequences than a progress
stamp has, and it wants its own issue.