Conversation
When the same object was tracked in two overlapping jobs and the track in the later job started earlier in the overlap, the united track returned by TrackManager._unite_objects() was never put back into the task annotations. The old track stayed without its closing outside shape and was propagated to the end of the task in exports, as a static ghost copy of the object. Related to cvat-ai#11204
VerificationResumed a capped verification run: What ran
Head Base All 5 new subtests fail on base, pass at head. The 11 pre-existing Boundaries rebuilt from the diffRebuilt the ledger from
Fails-before / passes-after for all 5 new subtests shown above. Remaining rows (2, 3, 4, 6, 13, 14, 15, 16) are pass-on-base cases (equal start frame, stored track starts earlier, unmatched objects, the Mutants re-run against the enlarged test classAll 6 mutants from the original verification (
No mutant survives unexpectedly. Reachability re-checked independently
Lint / format
Prior art / supersessionRe-checked before starting: ConclusionFix and regression coverage hold under mutation and reachability attack. Labelling Rules: ledger-row-needs-its-fixture=covered(test_merge_keeps_replaced_track_reachable, test_merge_keeps_track_starting_earlier_in_next_job_with_elements) | mutate-the-rejected-alternatives=covered(keep-old-track/append-united/rebind-list/move-to-end/drop-others mutants) | no-control-cases-in-the-suite=unreachable(no base-passing control was added to the suite; boundary rows that pass on base are kept as probes, not tests) | policy-section-verbatim=covered(## Policy quotes PULL_REQUEST_TEMPLATE.md, CHANGELOG.md, coding-style.md, pull-requests.md and running-tests.md verbatim) | prior-art-recheck-at-gate=covered(origin/develop unchanged, issues still open, no linked PR) | reads-as-generated=unreachable(test diff to production diff ratio ~15x, comparable to the merged sibling PR cvat-ai#9905's ~14x on the same file) |
sprayberry-redline
left a comment
There was a problem hiding this comment.
Automated review from the Sprayberry Labs fleet code reviewer.
Reviewed by the gating lane (gating review).
Verdict: approve. Reviewed at head d8df8e55dc7ac043cceb6659831536905aa363ff (base 1735290c0). The bug reproduces on the base by reading, the 12-line fix is the right layer, all five new test cases fail on the base and pass with the fix, no open upstream PR covers it, and the diff follows the upstream template.
What I verified
The bug is real on the base. Fetched cvat/apps/dataset_manager/annotation.py at 1735290c0 from cvat-ai/cvat and traced the regression fixture: job 1 (start 0) contributes an auto track at frame 6; job 2 (start 5, overlap 5) contributes a manual track with keyframes 5 and 13 (outside). In TrackManager.merge(), old_objects_by_frame = {0: [A]} and int_objects_by_frame = {0: [B]}. _calc_objects_similarity over frames 5..9 gives error 1 (frame 5 only in B) over count 5, cost 0.2 <= 0.5, so they match. _unite_objects(B, A) at base line 1221 picks track = obj0 because 5 < 6, folds A's frame-6 shape into B, and returns B. Base line 488 stores it as old_objects[j] = ..., a list local to the merge() call, and merge() at base lines 455-458 only extends self.objects with the unmatched new_objects. So self.objects still holds A with shapes [6] and no closing keyframe; to_shapes(15) propagates it through frame 14 and frame 5 is lost. That matches the body's base output [(1, [(6, False)])] / frames [6..14].
The fix. annotation.py:455-468 (head):
matched_old_objects = list(old_objects)
new_objects = self._merge_objects_on_one_frame(
int_objects, old_objects, start_frame, overlap
)
united_objects = {
id(old_obj): obj
for old_obj, obj in zip(matched_old_objects, old_objects)
if obj is not old_obj
}
if united_objects:
self.objects[:] = [united_objects.get(id(obj), obj) for obj in self.objects]_merge_objects_on_one_frame mutates old_objects only by index assignment, so the zip positions line up. Every keyed dict is kept alive by matched_old_objects until the comprehension finishes, so the id() keys cannot be recycled. The slice assignment updates the list that AnnotationIR.tracks shares with the manager, which is the one the caller reads. Shape and tag managers group candidates by frame, so obj0["frame"] < obj1["frame"] never holds there and the new branch is never taken for them; the streaming path calls _merge_objects_on_one_frame directly and is untouched.
The tests prove it. Traced all five cases on both arms:
test_merge_keeps_track_starting_earlier_in_next_job(2d, 3d): base yields('auto', [(6, False)]); fix yields('manual', [(5, False), (6, False), (13, True)]). Thesourceassertion is what kills the "always keep the stored track" alternative.test_merge_keeps_replaced_track_reachable"two tracks matched in the same merge": the diagonal costs are 0.2 and the off-diagonal boxes (base 10 vs 20) do not overlap, so both are replaced; base keeps bothautotracks.- same test, "united again by job 3": job 3's track (frame 11) matches the dict job 2 put in
self.objects;_unite_objectsreturns the stored dict this time, so the identity check is false and nothing is replaced. Base instead unites into the staleautotrack and gives[(6, False), (11, False), (16, True)]. ..._with_elements: the replaced parent carries job 2's elements; base leaves the parent at[(6, False)].
Verification comment at this head (issuecomment-5832883592) shows 5 FAIL on the base arm and 14/14 on head, consistent with my trace.
Prior art, re-run. gh search prs --repo cvat-ai/cvat for "unite_objects", "track merge overlap", "ghost track", "overlapping jobs track export", "11204", "8629": all empty. gh api repos/cvat-ai/cvat/compare/1735290c0...develop: 2 commits, neither touches dataset_manager/annotation.py or test_annotation.py. Issues cvat-ai#11204 and cvat-ai#8629 remain open with no linked PR.
Policy. No root CONTRIBUTING.md / AGENTS.md / AI policy at the base; the PR template's checklist items (develop branch, changelog fragment, tests, linked issue, MIT) are all satisfied. Fork CI: black, isort, pylint, bandit, eslint, spellcheck, remark and the rest all pass at this head.
Hygiene. One bug, +12 production lines, no unrelated changes. Commit messages carry no attribution trailer, no model name and no em dashes; neither do the diff, the title or the body.
Notes for the operator (non-blocking)
- Fork CI
build(and theunit_testingjob behind it) was still pending when I reviewed; the linters are green. Before submitting, either wait for that run or executepython manage.py test --settings cvat.settings.testing cvat.apps.dataset_manager.tests.test_annotation -v 2once, as the body already asks. - The changelog fragment is named
20260924_120000_fix_task_export_lost_track_merge.md; upstream's fragments come fromscriv createand carry the author segment (20260918_150106_aleksey.zinovyev_fix_track_playback_end.md). Consider regenerating the name with your username so it matches the neighbours. test_merge_keeps_track_starting_earlier_in_next_job_with_elementsalso asserts the element's shapes are[(5, False), (13, True)], i.e. that job 1's element keyframe at frame 6 is dropped. That is pre-existing behaviour ofTrackManager._unite_objects(it never mergeselements, on either branch) and not something this fix changes, but the assertion pins the limitation. If a maintainer asks why the element is missing frame 6, the honest answer is "it never merged elements"; dropping that second assertion, or asserting only that the element now ends with an outside keyframe, would avoid locking it in.- Upstream squash-merges; the five commits (fix + four test-only commits) are fine on the fork but the upstream PR will land as one.
Minor: the two-line comment above united_objects and the three-line comment opening test_merge_keeps_replaced_track_reachable could each be one line; not worth a round trip.
What I did not do: run the repository's Django test runner locally (CI is the signal) or read the Second Read's review.
|
Submitted upstream for review. |
Summary
ObjectManager.merge()calls_unite_objects(new, old). The result is written back only into a per-frame list local tomerge(), never intoself.objects.TrackManager._unite_objects()returns the incoming track dict when that track starts on an earlier frame than the stored one. In that case the united track, which carries both jobs' keyframes and the closingoutsidekeyframe, is dropped. The stored track from the earlier job stays in the task with no closing keyframe.to_shapes()propagates a track that has no closing keyframe to the end of the task. The result is a static copy of the object on every later frame (the "ghost annotations" of Extra ghost annotations in Task/Project export but not in Job export or UI cvat-ai/cvat#11204 and the "static ghost track" of Exporting object tracking task annotation from multiple jobs is not merging overlapping tracks cvat-ai/cvat#8629). The later job's first overlap keyframe is also lost.cvat/apps/dataset_manager/annotation.py, +12 lines inObjectManager.merge()): after each per-frame merge, any object whose united result is a different dict is replaced inself.objects, in place and matched by identity. No other behaviour changes. Shapes and tags never take the new branch (see Boundaries).AnnotationManagerTest.test_merge_keeps_track_starting_earlier_in_next_job(2D and 3D subtests) fails on base and passes with the fix. Two adversarial-verification tests,test_merge_keeps_replaced_track_reachable(2 subtests: two replacements in one merge, a replaced track united again by a third job) andtest_merge_keeps_track_starting_earlier_in_next_job_with_elements(skeleton parent + element), also fail on base and pass with the fix. Achangelog.dfragment is included, as the PR template requires.Upstream
cvat-ai/cvat, default branchdevelop.1735290c037b02466307de4d0f50fbe91041c8f4("Audio: fix create audio task with advanced config (Audio: fix create audio task with advanced config cvat-ai/cvat#11219)").origin/developwas still at this sha when rechecked on 2026-09-24 at about 13:35Z.d8df8e55dc7ac043cceb6659831536905aa363ffonsprayberry-code/cvatbranchfix/task-track-merge-lost(5 commits: the fix at77fdc1b65plus 4 test-only commits, the last from adversarial verification).cvat/apps/dataset_manager/annotation.py,ObjectManager.merge()(base lines 424-461),ObjectManager._merge_objects_on_one_frame()(base line 488:old_objects[j] = self._unite_objects(int_objects[i], old_objects[j])),TrackManager._unite_objects()(base line 1221:track = obj0 if obj0["frame"] < obj1["frame"] else obj1).cvat/apps/dataset_manager/task.pyTaskAnnotation._merge_data()->AnnotationManager.merge()->TrackManager.merge()for every job of a task, in segment order (task and project export,GET /api/tasks/<id>/annotations).changelog.d/20260924_120000_fix_task_export_lost_track_merge.md+6/-0,cvat/apps/dataset_manager/annotation.py+12/-0,cvat/apps/dataset_manager/tests/test_annotation.py+183/-1.Bug
The trigger is a task with overlapping jobs (
overlap> 0) where the same object is tracked in two adjacent jobs, and the later job's track starts on an earlier frame of the overlap than the earlier job's track. Each job's track is correct on its own, and the job export and the UI (which read one job at a time) show nothing wrong. When the task is assembled, the two tracks are matched as the same object.TrackManager._unite_objects()builds the combined keyframe list into the incoming dict, because that track starts earlier, and returns it._merge_objects_on_one_frame()stores that result inold_objects[j], a list built by_get_objects_by_frame()and local to themerge()call. It is then discarded.self.objects(the task'stracks) keeps the earlier job's dict unchanged: no closingoutsidekeyframe and none of the later job's keyframes. Task and project exports (and the task annotations API) then propagate the last keyframe of that track to the end of the task. This produces a static ghost box on every remaining frame and drops the later job's keyframes. Anyone who splits a tracking task into overlapping jobs and has objects entering the overlap at different frames in each job hits this. That is common with automatic trackers (the cvat-ai#11204 reporter used TransT) and with manual annotation (cvat-ai#8629's steps). The code path is old: the same local-list write existed inline inmerge()before cvat-ai#9782 (2025-09-16) factored it out, andgit log -Straces the line to the 2019 REST API rewrite (cvat-ai#389).Repro
Standalone script
repro.py. It loads the realcvat/apps/dataset_manager/annotation.pyfrom a checkout with only the Django-dependent imports stubbed (cvat.apps.engine.modelsenums,LabeledDataSerializer,rest_framework.serializers.ValidationError). The two helpers it needs fromutil.pyare taken verbatim. It merges two overlapping jobs the wayTaskAnnotation.init_from_dbdoes (AnnotationManager.merge(job, start_frame, overlap=5)for job 1 at 0 and job 2 at 5; task frames 0-14, segment size 10) and reads the result back throughAnnotationManager.to_shapes(15), keeping the non-outside shapes as the exporters do. The second case is the same object with job 1 starting first. It is included only to show that the matching works when the stored track wins.Base
1735290c0:With the fix (candidate head
d8df8e55d, production file byte-identical toc8e003ed0):Frames 13 and 14 on base are the ghost (the object left at 13). Frame 5 is the lost keyframe.
Fix
In
ObjectManager.merge(), the per-frame list is snapshotted before_merge_objects_on_one_frame()runs (matched_old_objects = list(old_objects)). Afterwards, each position where the list now holds a different dict than before (obj is not old_obj) is collected into a map keyed by the old dict'sid(). If that map is non-empty,self.objectsis rebuilt in place (self.objects[:] = ...), so the caller'sAnnotationIR.trackslist is updated, with each replaced dict swapped at its original position.Why this shape:
_merge_objects_on_one_frame()is also the merge callback ofStreamMerger(streamed shapes). There, the list it mutates is the output list, so rebinding by position is correct and must stay as is. Fixingmerge(), its other caller, leaves the streaming path untouched.use_server_track_idsis off (to_shapes()enumeratesself.objects).if united_objects:guard skips the O(len(self.objects)) rebuild for every frame where nothing changed. That covers every shape and tag merge (see Boundaries). With the guard, a 200-job, 190,050-shape task merge took 15.83s against 14.34s on base. Without it, 28.37s (guard_cost.py, same container, one run each).Alternatives rejected, each built as a mutant of the fixed file and run against the regression test (see Test evidence):
TrackManager._unite_objects()always return the stored track (track = obj1), with no change tomerge(). This fixes the shape of the stored track, but the result keeps the earlier job's track attributes,sourceandgroupinstead of those of the track that starts first. That is a behaviour change to what_unite_objectshas always chosen, and it leaves themerge()write-back latent for any other_unite_objects.self.objects = [...]: this only rebinds the manager's attribute. TheAnnotationIR.trackslist the caller holds is unchanged, so the bug remains.Test evidence
Regression test (in
cvat/apps/dataset_manager/tests/test_annotation.py, new classAnnotationManagerTestbetweenAnnotationIRTestandTestTaskAnnotation):test_merge_keeps_track_starting_earlier_in_next_job, subtestsdimension=2danddimension=3d. Job 1 (start 0) has anautotrack starting at 6 plus asemi-autotrack on frames 0-2 that ends before the overlap. Job 2 (start 5, overlap 5) has amanualtrack 5 -> 13 (outside). The test asserts (1) the task's tracks as(source, [(frame, outside), ...]), which must be exactly the unitedmanualtrack in place followed by the untouchedsemi-autotrack, and (2) the exported frames,[0, 1, 2, 5..13].Adversarial-verification tests, added at head
d8df8e55d:test_merge_keeps_replaced_track_reachable, subtest "two tracks matched in the same merge are both replaced": two stored tracks on the same frame, both matched by an earlier-starting job-2 track in one merge call. Asserts both are replaced (row 9).merge()put inself.objects. Asserts the job-3 keyframes land on the replaced dict (row 10).test_merge_keeps_track_starting_earlier_in_next_job_with_elements: a skeleton parent and its element both start earlier in job 2. Asserts the parent's shapes and the element's shapes are both the united result (row 11).1735290c0d8df8e55dAnnotationManagerTest.test_merge_keeps_track_starting_earlier_in_next_job[2d]AnnotationManagerTest.test_merge_keeps_track_starting_earlier_in_next_job[3d]AnnotationManagerTest.test_merge_keeps_replaced_track_reachable[two tracks matched in the same merge are both replaced]AnnotationManagerTest.test_merge_keeps_replaced_track_reachable[a track replaced by job 2 is united again by job 3]AnnotationManagerTest.test_merge_keeps_track_starting_earlier_in_next_job_with_elementsAnnotationIRTest+TrackManagerTestmerge())No base-passing control was added to the suite. The cases where the fix must not change anything (rows 2, 3, 4, 6, 13, 14, 15, 16) are in
## Boundariesas executed probe rows or reachability arguments, not in the suite, because they pass on base.What decides each assertion:
manualtrack with keyframes 5, 6, 13) and assertion (2): the replacement block (head lines 460-468). Base gives('auto', [(6, False)])and frames[0, 1, 2, 6..14].semi-autotrack still present, after the united one): theunited_objects.get(id(obj), obj)comprehension, which keeps every non-replaced object at its position.test_merge_keeps_replaced_track_reachable, both subtests: theid(old_obj)keys inunited_objects, built frommatched_old_objects(a snapshot taken before_merge_objects_on_one_frameruns), being distinct per stored object even when several are replaced in the same call, and the comprehension re-readingself.objectsafter each frame so a later merge sees the replaced dict already in place.test_merge_keeps_track_starting_earlier_in_next_job_with_elements: the replacement block operates onself.objects(the track list), which for a skeleton holds the parent; each element's ownmerge()call (viaTrackManager(track["elements"], ...), called fromto_shapes, not from this fix) is unaffected, so the element's shapes come from_unite_objects's ordinary return, carried inside the replaced parent dict.Mutants of
annotation.pyat head, each run againstAnnotationManagerTest(scriptmutants.py/mutants_extra.py), re-run atd8df8e55dagainst the full class including the two verification tests:keep-old-track: basemerge(),TrackManager._unite_objectsreturns the stored track (track = obj1)append-united:self.objects.extend(united_objects.values())rebind-list:self.objects = [...]move-to-end: remove replaced objects, append united ones at the endtest_merge_keeps_track_starting_earlier_in_next_jobonly (test_merge_keeps_replaced_track_reachableand the elements test still pass: neither exercises order among several stored tracks in one frame beyond what row 8's own regression test already covers)drop-others:self.objects[:] = list(united_objects.values())test_merge_keeps_track_starting_earlier_in_next_jobonly (same reason)no-guard: rebuild unconditionally (dropif united_objects:)Formatters (versions from
dev/requirements.txt:black==26.*,isort==7.*):Verification method
executed, with one stated gap.run_test.py. That script execs the realannotation.pywith the Django-only imports stubbed, takes the test module's helpers and the requested classes verbatim viaast, takescompare_objectsand its helpers verbatim fromcvat/apps/engine/tests/utils.py, and replacesdjango.test.TestCasewithunittest.TestCase(all these classes use).annotation.py, which is byte-identical to1735290c0(git diff 1735290c0 -- cvat/apps/dataset_manager/annotation.pyis empty there).TestTaskAnnotation(needs the DB; it mocksJobAnnotationand does not reach track merging), and the upstream commandpython manage.py test --settings cvat.settings.testing cvat/apps -v 2. The operator or upstream CI should runpython manage.py test --settings cvat.settings.testing cvat.apps.dataset_manager.tests.test_annotation -v 2once.gh pr checks 1 --repo sprayberry-code/cvatatd8df8e55dreportsno checks reported on the 'fix/task-track-merge-lost' branch. Actions are not enabled on the fork yet (operator card00MUFHWX70551B5BB62608EE10is pending), so there are no results rather than failures.Prior art
gh search prs --repo cvat-ai/cvatfor "unite_objects", "track merge overlap", "ghost annotations", "annotation.py merge tracks jobs", "static ghost track", "overlap track export" all returned[].gh search issues --repo cvat-ai/cvat "ghost track overlap"returned[].gh pr list --repo cvat-ai/cvat --search "11204 in:body" --state alland--search "8629 in:body" --state allboth returned[].git log 1735290c0..origin/develop: empty (no new commits).git log origin/develop --grep 11204and--grep 8629: empty.git log origin/develop -S_unite_objects -- cvat/apps/dataset_manager/annotation.py: only the 2020 app merge (Merge annotations and dataset_manager apps cvat-ai/cvat#1352).bug,need info): the maintainer asked for a task backup, and the reporter attached an exported annotations file with the note "objects219and220were created by the TransT tracker in the previous job and were not automatically finalized". The maintainer replied that it is not enough to reproduce. So this PR cites Extra ghost annotations in Task/Project export but not in Job export or UI cvat-ai/cvat#11204 as "Related", not "Fixes": the mechanism matches the reporter's note, but their data was never reproduced. Exporting object tracking task annotation from multiple jobs is not merging overlapping tracks cvat-ai/cvat#8629 "Exporting object tracking task annotation from multiple jobs is not merging overlapping tracks" (2024-11-01,bug, no maintainer reply) describes a track in job 1 and one in job 2 over the overlap, exported as two tracks. That is consistent with this defect, but its exact frames were not reproduced here either.Policy
At
1735290c0,CONTRIBUTING.md,.github/CONTRIBUTING.md,AGENTS.md,AI_POLICY.md,.github/AI_POLICY.md,AI.md,AGENT_POLICY.mdandCODE_OF_CONDUCT.mdare absent (git cat-file -efails for each). The contribution guide lives undersite/content/en/docs/contributing/. None of it mentions AI, LLMs or generated code (grep forAI|LLM|generated|copilot|agentmatched only unrelated text). No CLA or DCO..github/PULL_REQUEST_TEMPLATE.md@1735290c0, verbatim:CHANGELOG.md@1735290c0: "Developers: this project uses scriv (https://scriv.readthedocs.io/en/stable/index.html) to maintain the changelog. To add an entry, create a fragment:$ scriv create --edit". Complied with:changelog.d/20260924_120000_fix_task_export_lost_track_merge.md,### Fixed, in the style of the existing fragments.site/content/en/docs/contributing/coding-style.md@1735290c0: "For Python, we use Black and isort to enforce the coding style and autoformat files. You can usedev/format_python_code.shto apply these formatters." Complied with: black 26.5.1 and isort 7.0.0 are clean on both touched Python files. CI'slinters.ymlrunsblack --check --diff .and isort.site/content/en/docs/contributing/pull-requests.md@1735290c0: "They should remain focused in scope and avoid containing unrelated commits." and "Please ask first before embarking on any significant pull request (e.g. implementing features, refactoring code, porting to a different language)". This is a 12-line bug fix, not a significant PR under that sentence.site/content/en/docs/contributing/running-tests.md@1735290c0: "python manage.py test --settings cvat.settings.testing cvat/apps -v 2". Not run here (no Django/DB, see Verification method).Disclosure facts for the operator
annotation.py, and wroterepro.pyand therun_test.pyharness.test_merge_keeps_replaced_track_reachableandtest_merge_keeps_track_starting_earlier_in_next_job_with_elementsto close the three rows that had been probe-only, re-ran all six mutants against the enlarged test class, and re-ran black and isort. It did not run Django's test runner or the full suite.manage.py testcommand above before submitting.Boundaries
Head line numbers refer to
annotation.pyatd8df8e55d(production file byte-identical toc8e003ed0). "Probe" rows were executed withprobes.py/probe6.pyon both arms (output below the table); they are deliberately not in the suite, because they pass on base. Rows 9, 10 and 11 were probe-only throughc8e003ed0and are now also pinned by regression tests added during adversarial verification (see Test evidence).obj is not old_obj(465): united result is the incoming dictself.objectsobj is not old_obj: united result is the stored dict_unite_objectspicksobj1on ties)[(1, 'auto', [(5, False), (13, True)])]); repro's second caseobj is not old_obj: stored track starts earlierobj is not old_obj: stored object unmatched (cost above threshold)old_objects[j]untouched, no entry;_modify_unmatched_objectcloses it atstart + overlapexactly as baseself.objectsnot among the frame's candidatesoutsidebeforestart_frame(filtered byTrackManager._get_objects_by_frame)semi-autoelement), killed mutantsdrop-others,move-to-endif united_objects:(467), falsy but valid:{}no-guardsurvives by design (performance only, measured 15.83s vs 28.37s)self.objects[:] =(468) in-place vs rebindAnnotationIR.tracks)rebind-listself.objectsmove-to-end; probe P3 (fix keeps3, 2, 1)test_merge_keeps_replaced_track_reachable[two tracks matched...] (FAIL on base, pass on head); probe P6 (base[(1, 'auto', [(6, False)]), (3, 'auto', [(7, False)])], fix bothmanualwith closing keyframes)test_merge_keeps_replaced_track_reachable[a track replaced by job 2 is united again by job 3] (FAIL on base, pass on head); probe P4 (fix[(2, 'manual', [(5, False), (6, False), (11, False), (12, False), (16, True)])]; base keepsautotrack without the frame-5 keyframe)elements)test_merge_keeps_track_starting_earlier_in_next_job_with_elements(FAIL on base, pass on head); probe P5 (fix element 20[(5, False), (13, True)]; base element[(6, False)])2d,3d(both fail on base)ShapeManager,TagManager_unite_objects)obj0["frame"] < obj1["frame"]is never trueobj1(stored), row 6 applies, no behaviour changeStreamMergerpath (merge_stream)_merge_objects_on_one_framewrites intoresult_objects, which is the yielded listmerge()changed; tracks never stream,AnnotationManager.mergealways callstracks.merge)id()keysmatched_old_objectsuntil the comprehension ends, so ids are unique and liveProbe output (
probes.txt), base then fix:Suggested upstream PR title
Fix lost track merge in task annotations when a track starts earlier in the next job