Skip to content

[Bug] : sync-omitted stage runs measure stale scratch cache, poisoning the catalog with old evidence under new content hashes #536

Description

@Sagar-024

Version or commit

Current main

Environment

Ubuntu 24.04 (WSL2), Python 3.12, x86_64, file:// obstore root

Minimal reproduction

Using a file:// bucket-backed storage root with independent worker mirrors:

  1. Worker 1 runs the full profile on clean footage (v1). Scratch is populated, measurements pass (black_frame_pct=0.00).
  2. The canonical object in the remote store is overwritten with altered camera payload (v2, bumped pipeline_version, true black_frame_pct=50.00). Worker 2 processes this and records the true v2 measurements.
  3. Worker 1 runs a sync-omitted profile (e.g., META + MEDIA only). Its etag-verified fetch downloads the new v2 canonical, but the scratch directory is not invalidated.

Expected behavior

Worker 1 fetches the new v2 canonical, invalidates or bypasses the stale scratch cache, measures the v2 pixels, and records the true v2 measurements (50.00) under the new episode_id.

Actual behavior

Worker 1 fetches the v2 canonical but keeps the v1 scratch (identical sha and mtime_ns). It measures the v1 pixels and records v1 measurements (0.00) under the v2 episode_id.

Because the stale run's measurement values differ from the truth, its _run_fingerprint is unique, so the catalog appends it as a new row. measurements_latest ranks by recorded_at, so the stale, newer measurement wins the wide view.

Worse, the poisoning is self-perpetuating: if a fresh worker (Worker 3) later tries to repair this by re-measuring, it produces the true values, which match the original v2 fingerprint. The catalog dedupes the repair (written=False), and it fails to overwrite the stale latest view. The system actively protects the lie.

Additional context

Root cause: The only scratch invalidation is shutil.rmtree(scratch_dir) at app.py:2391, guarded by if Stage.SYNC in enabled_stages (app.py:2347). The sync-omitted route (app.py:2395-2412) fetches the new canonical but never touches the scratch directory. Episode.video() relies on a bare output.exists() check (episode.py:551-554), serving the stale remux.

Why it matters: Curation cuts and gates accept or reject episodes using footage that isn't the footage. The downstream training corpus inherits mislabeled quality verdicts that survive the exact byte-change the content-hash was supposed to catch. The append-only deduplication logic (_run_fingerprint) weaponizes the repair path, making the poison durable.

Fix direction: Make the scratch content-bound: key scratch_dir on content_episode_id(canonical_path), or drop a digest marker inside the scratch dir and remux whenever it disagrees with the fetched canonical. Additionally, the measurement deduplication logic needs a retraction or force-overwrite path for later-arriving stale appends that lose to the truth.

Definition of done:

  • A sync-omitted run on a worker with stale scratch correctly measures the newly fetched canonical bytes, not the old ones.
  • _run_fingerprint deduplication does not allow a stale measurement to permanently shadow a true measurement in the latest views.
  • Mutation proof: tamper the canonical payload without touching scratch, and the recorded measurements must change.
  • Existing app/storage/catalog suites stay green.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    advancedNeeds codebase familiarity; not a starter issuebugSomething isn't workinghelp wantedExtra attention is needed

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions