On the enterprise workflow platform, a task moving through review can be claimed, released, reworked, and re-claimed by a different reviewer — sometimes several times before it's done. The original design cleared a reviewer's draft state the moment a claim was released, on the theory that a fresh claimant should start clean. In an auditable training-data pipeline, that theory is wrong: clearing state destroys exactly the record you need when something needs to be traced back later. AGT-1431 replaced claim-time draft clearing with roll-forward versioning — release moves the task forward to a new version rather than erasing what was there. That one architectural change is small to describe and took the rest of the month to get right.
Versioning forward instead of clearing in place is the correct call for auditability — and it moves every assumption your navigation, lineage, and concurrency code was quietly making.
The core change: release moves forward, it doesn't erase
PR #831 (AGT-1431) is the foundation: manual release and timeout release both now roll the task forward to a new version instead of clearing the current draft. The PR shipped with a shared version-copy policy, release async guards, assignment-release metadata events, and lineage exposed through the version API and UI — plus, notably, a documented roll-forward contract and validation checklist committed alongside the code. That documentation mattered more than it might look: almost every bug that followed was a case of some other part of the system not yet knowing the new contract existed.
Navigation bugs: correct data, wrong screen
PR #869 (AGT-1593, AGT-1594) found two bugs introduced by the roll-forward change, and the first is a good reminder that "the data is right" and "the feature works" are different claims. A same-user re-claim was redirecting to the previous version instead of the current head — a different user claiming the same task redirected correctly, so the underlying versioning was sound; only the same-user navigation path had the bug. The second, AGT-1594, was a lineage-tracking bug: a source version was supposed to stay pinned at its base (v4) but was incrementing on every subsequent action, so the version dropdown showed "v6 (Source: v5)" when it should have shown "v6 (Source: v4)" — the lineage pointer was chasing the current version instead of recording where the chain actually started.
A redirect that fired for every version, not just the rework source
PR #988 (AGT-1760) is the sharpest of the navigation bugs. Golden-data rework forks a task to a new version, and an effect was supposed to redirect a viewer straight to that new version — but the condition triggering the redirect was simply "the manifest's version is greater than whatever version you're looking at." That's true for every older version, not just the one the fork actually copied from. The practical effect: an admin trying to inspect version history on a task with a pending rework got redirected away from v1, v2, and v3 alike, whenever the manifest sat at v4 — making it impossible to look at anything but the newest version while a rework was in flight. The fix extracts the redirect condition into its own named function, scoped specifically to the rework's actual source version, so inspecting history stops fighting the same redirect that's supposed to help active reviewers.
Cross-task lineage and a 500 that only fired on clones
PR #893 (AGT-1604) traced a 500 error on the version-history endpoint back to lineage resolution rejecting a legitimate shape: a test-task clone whose version chain contained a cross-task terminal ancestor in the middle of the chain, not just as the starting point. The resolver had only ever been exercised with a terminal ancestor as an origin; a chain that passed through one and continued was outside what it had been built to expect, and it failed closed with a 500 instead of resolving. The fix widens acceptance to cover cross-task terminals wherever they legitimately appear in a chain, verified against two real QA tasks pulled directly from Cloud Run logs — the kind of bug that specifically needs production traces to notice, since it only manifests on a lineage shape that generated tests are unlikely to construct by accident.
A GCS marker race that flashed a false failure
PR #894, following directly from #893, chased down a brief "task version resolution failed" flash that appeared every time a user opened a task from the dashboard. Two root causes stacked: server-side, an unpinned GCS read had no 404 handling around the case where a version's visibility marker exists as a prefix but is still mid-write — a genuine TOCTOU race between roll-forward, a calibration fork, or initial version creation writing the marker, and a read arriving in that same narrow window. Client-side, the auto-seed list page was separately navigating to a stale version. The fix normalizes the 404 race so it's treated as "still loading," not "failed," and keeps the loading state visible through legitimate retries instead of surfacing a transient race as a hard error to the user.
The race that actually wedged a task
PR #987 (AGT-1759) is the one real concurrency bug in this cluster, found live on QA: a specific task was stuck with v4's golden-data step showing a QC error while v5 sat in the trainer review gate — a genuinely confusing state for whoever opened it next. The root cause was concurrent calls to the version-resolution function racing to fork the same rework version through a copy-then-write-then-commit sequence against GCS. Two forks landing on the same source version at the same time collided on the compare-and-swap for the result file, and the copy failed outright with a parallel-copy error. The fix serializes concurrent golden-data QC-rework forks, closing the exact window where two reviewers (or a reviewer and a retry) could both try to fork the same version at once.
PR #981 (AGT-1354) is the PR that originally built the fork-on-rework mechanism this race lived inside — rebased onto main after the shared QC engine adapter (from a separate PR stack, #841–#843) had landed independently and implemented an overlapping piece of the same file. Rather than pick one implementation and discard the other, the rebase combined both: the shared adapter's load/writeStatus/finalizeOnPass machinery stayed, and roughly 640 lines of the original PR's custom copy/rollback/resolve infrastructure were replaced by calls into that shared adapter instead of a parallel implementation living next to it. Reconciling two independently-built solutions to the same problem into one, rather than shipping whichever landed second, is the less obvious and more valuable version of "resolving a merge conflict."
A conflict filter with no memory of "released"
PR #990 (AGT-1761) is a smaller bug with an outsized effect on reviewer experience: the task pool hides tasks from a user holding a conflicting prior assignment, but all four of the exclusion predicates matched on status alone (CLAIMED or CLOSED) with no check on why the assignment closed. A user who claimed a task and then released it back to the pool was hidden from it identically to the user who actually submitted it — meaning releasing a task could permanently lock the releaser out of ever claiming it again, the opposite of what "release" is supposed to mean. The fix uses the platform's existing release-reason constants to exclude only genuinely-closed (submitted) assignments from the conflict filter, letting a released claim go back into the open pool the way it should.
Roll-forward's ripple into a separate storage tier
PR #896 (AGT-1606) found that roll-forward versioning, as originally shipped, only copied a version tree into the primary storage bucket — nothing touched the long-horizon storage bucket used for larger, hour-scale tasks. Both the golden and model regrade dispatch paths assumed a specific package existed in that second bucket regardless, and failed with an opaque rsync error when it didn't. The fix self-heals the missing package and makes sure a required pipeline step runs before a regrade dispatch fires, rather than assuming a prerequisite that roll-forward's original scope had silently left out.
Two honest error-message fixes
Two smaller PRs are worth including because they're a different kind of fix — not wrong behavior, just an error message that hid the real problem. PR #895 (AGT-1604) replaced a generic "Failed to resolve task versions" fallback with a descriptive error carrying the task ID, the failure stage, and the underlying cause, because the generic version made it impossible to diagnose anything on beta or QA without cross-referencing server logs by hand. PR #1006 fixed the same class of problem on the admin side: an uploaded QC feedback file in the wrong shape produced a raw Zod validation message — "Invalid input: expected string, received undefined" — with no field name and no hint at the expected shape, leaving an admin to guess which of several possible fields was missing.
Shipping a whole second review path alongside the first
Separately from the versioning work, PRs #941 and #943 shipped a complete manual-QC review path as a new first-class alternative to the existing automated one. #941 laid reusable infrastructure first — auth guards and services, a Cloud Tasks integration with idempotency and retry, structured operation logging, and configuration for runtime roles — deliberately with no manual-QC-specific code in the same PR. #943 then built the actual Next.js UI on top of that infrastructure: a three-pane review console, a typed API client mirroring the backend's DTOs, and React Query hooks for review, submit, and transfer actions. Landing the reusable plumbing and the feature-specific UI as two separate, sequential PRs — rather than one large PR mixing both — is the same discipline as the roll-forward contract doc: each piece is reviewable and revertible on its own.
The pattern: an architectural fix moves the bugs, it doesn't remove them
Every bug in this cluster traces back to one change: release now versions forward instead of clearing in place. That change was correct — clearing draft state on release genuinely was the wrong design for an auditable pipeline. But "correct architecture" and "correct in every caller" are different claims, and this month is the gap between them: navigation code that assumed release meant "start over" redirected to the wrong place, lineage tracking that assumed a source version was static drifted, a second storage tier the original change never touched broke silently, and concurrent access to the new fork-on-rework path raced in a way the old clear-in-place design never could have, because it never had two versions to race between. None of that is a reason not to make the architectural fix. It's the reason an architectural fix needs a month of follow-through, not a day.