Skip to content

fix(sync): terminalize cloud_tasks jobs whose dispatch was lost before any attempt#10098

Merged
undivisible merged 2 commits into
BasedHardware:mainfrom
aryanorastar:fix/10033-sync-dispatch-lost
Jul 24, 2026
Merged

fix(sync): terminalize cloud_tasks jobs whose dispatch was lost before any attempt#10098
undivisible merged 2 commits into
BasedHardware:mainfrom
aryanorastar:fix/10033-sync-dispatch-lost

Conversation

@aryanorastar

@aryanorastar aryanorastar commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Summary

  • A cloud_tasks sync job whose named-task dispatch was lost before any worker attempt now goes stale after 30 minutes and is terminalized by the existing owned stale finalizer with a distinct sync_dispatch_lost code, instead of sitting queued until the 24h JOB_TTL expiry while the app shows Uploaded · processing on Omi the whole time.
  • The client's existing failed-job handling then takes over: WALs revert to miss and re-upload from the retained local files on the next sync pass — no app change needed.
  • Fixes the stuck-queue symptom of Bug: offline recordings fail to sync, fail to create conversations, and remain stuck in queue #10033 (server side): the reported 12+-hour Uploaded · processing on Omi items match this envelope exactly (stuck < 24h TTL, then eventual 404 recovery).

Root cause

The upload route creates the Redis job (status='queued', dispatch_mode='cloud_tasks') before enqueueing the named Cloud Task (deliberately, so an ambiguous enqueue can never start an unfenced inline twin). When the dispatch is lost — both enqueue attempts fail (the enqueue_uncertain path returns 202 and preserves all durable inputs, correctly refusing inline work), or the task is purged — nothing ever transitions the job again:

  • is_sync_job_stale only considered status == 'processing' ("queued jobs are intentionally never stale"), so the poll-route stale finalizer never fired.
  • The app reconciler's notFound recovery only triggers at job-TTL expiry, 24 hours later.

The stuck job is a zombie the whole window: client polls every pass, server truthfully answers queued, nobody owns the exit.

Failure class

Failure-Class: none

Closest is FC-session-scoped-durable-work (durable work must not inherit dispatcher lifetime), but that class covers work cancelled with its session; this is durable work whose dispatch signal was lost with no owner for the never-started case. Not stretching the definition.

What is deliberately preserved

  • sync-job stale guard incorrectly marks healthy queued jobs as 'failed' (background-busy is mislabelled) #7469 contract: inline queued jobs are never stale at any age — a saturated pool is not a failure. The new staleness requires dispatch_mode == 'cloud_tasks'.
  • Retry-backoff contract: mark_job_queued_for_retry re-queues between Cloud Tasks attempts and relies on queued jobs being poll-safe. Those jobs carry attempt/started_at; the new staleness requires both unset (never-attempted), so a pending retry is never flipped terminal however long its backoff (GCP default max backoff is 1h — a naive age check would have broken this).
  • Late task safety: a task that fires after terminalization hits the worker's existing terminal-status guard — acks 200, drops staged blobs, releases the claim. Content-claim dedup makes the client's re-upload cheap.
  • The finalization path is the existing lease-owned one (try_acquire_sync_job_run_lock → re-read → finalize_sync_job_failure_now); fenced_mark_job_failed already allows queued → failed, and a polling read still never publishes a terminal state without the lease.

Product invariants affected

none (verified with scripts/pr-preflight --suggest)

Validation

  • Old-code check (fix stashed): the new staleness-matrix unit test and the route-level sync_dispatch_lost finalization test → 2 failed; with the fix → both pass.
  • Staleness matrix covered: never-dispatched cloud_tasks job past threshold → stale; under threshold → not; inline at any age → not (sync-job stale guard incorrectly marks healthy queued jobs as 'failed' (background-busy is mislabelled) #7469); pending retry (attempt set) at any age → not.
  • Repo runner (file-isolated, as CI executes): tests/unit/test_sync_v2.py + tests/unit/test_sync_cloud_tasks.py82 passed.
  • make preflight (local lane, this PR body) → all selected checks pass.
  • black clean on all changed files.
  • backend/routers/sync.py frozen-size baseline raised 2024 → 2026 (+2 for the queued-dispatch-lost finalization path) with justification, per the ratchet's documented raise path.

Not exercised live: a real Cloud Tasks queue purge against production (no live services here). The enqueue-uncertain code path that produces this state is deterministic in source; the tests drive the staleness decision and the route finalization directly.

Out of scope, named

Review in cubic

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@Git-on-my-level Git-on-my-level added needs-maintainer-review Needs a human maintainer to sign off before merge security-review Touches auth, provider routing, secrets, or security-sensitive surfaces labels Jul 20, 2026
@Git-on-my-level

Copy link
Copy Markdown
Collaborator

Thanks for the careful write-up and tests here. This is a positive signal from automation: the change is narrow, preserves the inline-queued and retry-backoff contracts, reuses the existing lease-owned stale finalizer, and adds focused coverage for the never-dispatched cloud_tasks case plus the distinct sync_dispatch_lost code.

I’m not formally approving because this touches sync job terminalization/retry recovery for retained user audio and changes the timing/semantics of when a queued Cloud Tasks job becomes terminal. A human maintainer should still sign off on the operational threshold and recovery behavior before merge.

No supply-chain or obvious malicious behavior found in the diff. The relevant backend/Cloud Tasks checks are passing on this head.


by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.

@kodjima33 kodjima33 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed by automated PR reviewer — approving; holding merge (see notes).

@aryanorastar

Copy link
Copy Markdown
Contributor Author

@Git-on-my-level @undivisible This PR is fully approved. I've pushed a fix for the pre-existing Backend unit suite timebomb that was failing CI (the short-term lifecycle tests hardcoded June 20, 2026 and hit +30 days expiry validation failures today). CI should be green shortly. Could you please merge when you have a chance?

@aryanorastar

Copy link
Copy Markdown
Contributor Author

Thanks for the review pass. Backend unit suite is green on this head; ready for a maintainer's final call on the queued-dispatch stale threshold (30m) whenever there's bandwidth.

@aryanorastar

Copy link
Copy Markdown
Contributor Author

@kodjima33 approved and green — all checks pass (Backend unit suite, Hermetic E2E, Sync Cloud Tasks gauntlet). Just needs a maintainer sign-off on the 30-minute queued-dispatch stale threshold, which is a single constant. Ready to merge whenever you lift the hold. Thanks!

@Git-on-my-level

Copy link
Copy Markdown
Collaborator

Rechecked the current head after the follow-up test-only commit. Still a positive signal from automation: the sync change stays narrow, keeps inline queued jobs and Cloud Tasks retry backoff out of poll-terminalization, uses the existing lease-owned stale finalizer, and now has focused coverage for the sync_dispatch_lost path plus the timebombed unit test fix.

I’m still not formally approving because this is a security/user-data recovery surface: it changes when retained-audio sync jobs become terminal and depends on the 30-minute lost-dispatch threshold being the right operational tradeoff. Human maintainer sign-off should remain required before merge.

No supply-chain or obvious malicious behavior found in the current diff; relevant backend/Cloud Tasks checks are green.


by AI on behalf of David — if you need David’s attention urgently, please @Git-on-my-level and escalate with need human response.

@aryanorastar

Copy link
Copy Markdown
Contributor Author

@Git-on-my-level need human response — requesting David's merge sign-off. Approved by @kodjima33, all checks green (Backend unit suite, Hermetic E2E, Sync Cloud Tasks gauntlet), no conflicts. The only open item your reviews flagged is the operational call on the 30-minute lost-dispatch stale threshold — it's a single constant (QUEUED_DISPATCH_STALE_SECONDS), easily tuned if you'd prefer a different value. Ready to merge.

kodjima33 pushed a commit that referenced this pull request Jul 21, 2026
…10119)

* fix(sync): never merge synced audio into a soft-deleted conversation

get_closest_conversation_to_timestamps matched any conversation in its
±2-minute window — including deleted: True tombstones — so offline audio
recorded near a conversation the user later deleted merged its segments
into the tombstone and vanished: uploads reported success, the job
completed, and no visible conversation ever appeared (#10033's
never-creates-a-conversation symptom). The auto-sync target-attach path
had the same hole through its direct get_conversation fetch.

The closest-match choice is now a pure selector (select_closest_conversation)
over eligible merge targets, with one shared predicate
(eligible_merge_target) excluding soft-deleted rows; the target-attach
path consults the same predicate and falls back to the filtered
timestamp lookup. A window containing only tombstones now creates a
fresh conversation instead of feeding the dead one. Discarded rows stay
eligible — the merge path already reprocesses and revives them.

Regression tests (fail on old code — the selector did not exist and the
inline loop accepted tombstones): deleted-only window yields None, a
closer deleted row loses to a live one, boundary-distance choice and the
predicate matrix, plus a labeled source tripwire that the target-attach
path gates through the shared predicate.

Fixes the never-creates-a-conversation symptom of #10033 (server side);
the stuck-queue symptom is #10098.

Failure-Class: none

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(sync): behavior test for target attach fallback & fix test timebomb

* test(sync): allowlist the target-attach behavior test for the fast-unit duration guard

The behavior test added for the #10119 review (drives the real
process_segment target-attach fallback) is the sole heavy test in its
file, so it amortizes the sync-pipeline import graph into its call phase
and exceeds the 0.12s fast-unit CPU budget — the same file-isolation
structural cost the allowlist already grandfathers for
test_sync_silent_failure.py::TestProcessSegmentReal. Add its node id
alongside, per the allowlist's documented purpose.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@aryanorastar
aryanorastar force-pushed the fix/10033-sync-dispatch-lost branch 2 times, most recently from ee1ad88 to 5c5bba9 Compare July 22, 2026 18:31
@aryanorastar

Copy link
Copy Markdown
Contributor Author

@Git-on-my-level need human response — this PR is approved, rebased onto latest main today, and all checks are green (mergeable, clean). Ready for a maintainer to merge (regular merge, no squash). Thanks!

aryanorastar and others added 2 commits July 24, 2026 06:44
…e any attempt

An uploaded batch creates the Redis sync job ('queued') and then enqueues
the named Cloud Task. When that dispatch is lost — both enqueue attempts
fail (the enqueue_uncertain path deliberately returns 202 and never
starts inline work), or the queue is purged — nothing ever transitions
the job again: is_sync_job_stale only considered 'processing', so the
poll-route stale finalizer never fired and the client polled
'Uploaded · processing on Omi' until the 24h JOB_TTL expiry (BasedHardware#10033).

A cloud_tasks job still 'queued' with no attempt ever started
(started_at/attempt unset) now goes stale after 30 minutes and the
existing owned stale finalizer terminalizes it with a distinct
'sync_dispatch_lost' code, releasing the content claim. The client's
existing failed-job handling then reverts the WALs to miss and
re-uploads from the retained local files.

The two neighboring contracts are preserved exactly: inline queued jobs
are never stale at any age (BasedHardware#7469 pool saturation), and a worker
re-queue between Cloud Tasks retries (mark_job_queued_for_retry, which
sets attempt/started_at) is never poll-terminalized however long its
backoff. A task that fires after terminalization already acks and drops
via the worker's terminal-status guard.

Regression tests: the staleness matrix (never-dispatched stale;
under-threshold, inline, and pending-retry not stale) fails on the old
code, and a route-level test proves the queued finalization publishes
'sync_dispatch_lost'.

Fixes the stuck-queue symptom of BasedHardware#10033 (server side).

Failure-Class: none

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…spatch runbook

The enqueue-uncertainty runbook described the queued job as pollable
with only TTL-expiry recovery; the new 30-minute sync_dispatch_lost
terminalization changes what an operator observes and should check.
Docs move with the code (DoD rule 6) — belongs in this PR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@aryanorastar
aryanorastar force-pushed the fix/10033-sync-dispatch-lost branch from 5c5bba9 to ae938fc Compare July 24, 2026 01:16
@aryanorastar

Copy link
Copy Markdown
Contributor Author

@Git-on-my-level need human response — batching these so they don't need chasing one at a time.

Six PRs are approved, green, and mergeable right now (all verified APPROVED + MERGEABLE + CLEAN, zero failing checks, as of this comment):

PR
#10098 fix(sync): terminalize cloud_tasks jobs whose dispatch was lost before any attempt (this one)
#10101 harden(desktop-backend): bounded deadline on the bare Client::new() call sites
#10126 refactor(app): trust the server's single_language_mode, drop the client mirror
#10128 chore(desktop-backend): scope the unwrap_used deny to production code
#10170 test(app): pin the template install-failure polarity as a pure function
#10262 fix(conversations): reject soft-deleted conversations from merge

Regular merge, no squash, per the contributing guide.

#10060 and #10270 are also approved; both were just rebased onto latest main and their CI is re-running.

No urgency on any of these — posting once as a batch rather than pinging each thread. Happy to rebase any that go stale.

@undivisible
undivisible merged commit 1f13c1d into BasedHardware:main Jul 24, 2026
31 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-maintainer-review Needs a human maintainer to sign off before merge security-review Touches auth, provider routing, secrets, or security-sensitive surfaces

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants