Skip to content

Desktop updates: explicit channel pointers, qualified beta releases, and observable legacy fallback #9350

Description

@Git-on-my-level

Summary

Replace implicit desktop channel selection from the first page of GitHub Releases with explicit stable/beta pointers, make the pointer path primary with a temporary observable legacy fallback, qualify beta candidates before exposure, and add a supported local head-of-main workflow for developers.

This is one umbrella tracker so the dual-read cleanup and deferred static-delivery work are not lost. Stable promotion remains a separate manual production decision; this issue must not advance the stable pointer without explicit approval.

Current risks

  • The Python appcast path scans only GitHub's first 100 releases. The current stable release can disappear as newer betas push it off that page.
  • The stable manual-download path currently falls back to beta when no stable release is found.
  • GitHub release-body fields are mutable and are being used as the channel database.
  • Every desktop change can currently become a public beta after only a quiet window.
  • Update Installed is emitted before replacement/relaunch success.
  • The existing rescue/update-policy surface shares the main API delivery path.
  • A temporary dual path can become permanent unless removal criteria are tracked now.

Architecture decision

Explicit manifests and pointers

Use Firestore as the interim control plane:

  • desktop_release_manifests/{release_id}
    • source SHA, version/build, GitHub ZIP/DMG URLs, SHA-256 digests, sizes, Sparkle EdDSA signature, qualification evidence, creation time
  • desktop_update_channels/macos
    • generation, exact stable_release_id, exact beta_release_id, promotion metadata

Release manifests are treated as immutable after registration. Pointer updates use a Firestore transaction / generation compare-and-swap and may only reference a complete validated manifest.

The API continues generating the Sparkle appcast and serving manual-download routes for now. Static GCS/CDN appcasts and artifacts are explicitly deferred below.

Primary/fallback order

  1. Valid live Firestore pointer snapshot
  2. Validated pointer last-known-good snapshot
  3. Legacy GitHub-release scan fallback
  4. Fail closed if none can produce the requested channel

Even on the legacy fallback, stable must never resolve to beta.

Keep a kill switch to force the legacy path during rollout. Run a cached/sampled reconciliation that evaluates both methods and reports semantic mismatches without putting a second GitHub request on every client request.

Required observability

Structured logs and metrics must distinguish:

  • source used: pointer_live, pointer_lkg, legacy_github, none
  • fallback reason: missing pointer, incomplete manifest, Firestore error, generation conflict, invalid signature/digest metadata, missing channel, stale pointer
  • pointer generation and age
  • resolved stable/beta release IDs and builds
  • pointer-vs-legacy mismatch by channel/field
  • LKG usage and age
  • appcast semantic validity and item count per channel
  • artifact HEAD/Range reachability

Do not log raw user identifiers or secrets.

Suggested counters/gauges:

  • desktop_update_resolution_total{source,channel}
  • desktop_update_fallback_total{reason,channel}
  • desktop_update_pointer_mismatch_total{channel,field}
  • desktop_update_pointer_age_seconds{channel}
  • desktop_update_lkg_age_seconds
  • desktop_update_feed_valid{channel}

Alert on any stable→beta crossover attempt, invalid/missing stable, sustained legacy fallback, stale pointers, semantic mismatches, or a 200 appcast without the expected channels.

Phase 1 — Pointer control plane and safe dual path

  • Define and test Firestore manifest/pointer schemas.
  • Add transactional generation/CAS pointer updates.
  • Seed manifests and pointers from the currently advertised stable/beta releases without advancing either channel.
  • Implement pointer-based appcast and manual-download resolution.
  • Make pointer resolution primary behind a feature flag.
  • Add validated long-lived pointer LKG; never overwrite it with incomplete data.
  • Preserve LKG on routine cache clears.
  • Add legacy GitHub scan fallback with explicit fallback reasons.
  • Remove stable→beta fallback from every resolution path.
  • Add cached/sampled pointer-vs-legacy reconciliation.
  • Add structured logs, metrics, alerts, and an operator dashboard.
  • Add semantic probes for exact stable/beta items, signatures, asset URLs, and channel separation.
  • Run in shadow mode, then switch primary via configuration after parity is established.
  • Add a rollback/kill-switch runbook.

Phase 2 — Qualified beta candidates

  • Stop automatically exposing every desktop-main change as beta.
  • Add scheduled candidate nomination plus manual nomination for urgent testing.
  • Build/sign/notarize the candidate before visibility; failed candidates must leave beta unchanged.
  • Produce immutable qualification evidence bound to source SHA and exact artifact digests.
  • Make these universally blocking:
    • clean release compile and component tests
    • T2 hermetic desktop core E2E
    • fault-injection suite
    • signed/notarized artifact audit and launch smoke
    • dependency/secret audit and bundle-size budget
    • appcast/pointer/signature/digest validation
    • previous-beta → candidate Sparkle update and confirmed relaunch
    • current-stable → candidate Sparkle update and confirmed relaunch
  • Add change-aware gates:
    • agent/chat/runtime → T3 continuity gauntlet
    • auth/keychain → hardening auth probes
    • audio/recording → capture/PTT lifecycle
    • storage/schema → previous-version migration fixture
    • updater/release changes → expanded Sparkle matrix
    • backend contract changes → local workflow contracts
  • Keep full live T3/performance/soak runs scheduled/advisory except when the changed surface makes them blocking.
  • Advance only the beta pointer after the qualification gate passes.
  • Add an explicit break-glass override with typed confirmation and recorded evidence.

Phase 3 — Supported developer head-of-main

  • Add a repo-owned desktop/macos/scripts/omi-main tool with install, update, and status.
  • Use a dedicated worktree tracking origin/main.
  • Install a dedicated /Applications/omi-main.app / bundle ID; never replace Omi, Omi Beta, or Omi Dev.
  • Reuse incremental build caches and stamp OMISelfBuild=true.
  • Seed auth/settings from Omi Dev.
  • Check recording/update activity before atomic replacement and relaunch.
  • Add watch only after manual update/relaunch is proven reliable.
  • Document local signing/TCC expectations and recovery.

Phase 4 — Update transaction truth

  • Persist an update attempt ID and phase across relaunch.
  • Record discovered/downloaded/verified/staged/install-started separately.
  • Emit installed/succeeded only after the expected new build launches and confirms health.
  • Track consecutive failures and suppress one-off background-network noise.
  • Provide a sanitized Copy Update Diagnostics/support code.

Phase 5 — Deferred static delivery and independent rescue

Tracked here intentionally, but may be split into implementation issues when started:

  • Move immutable ZIP/DMG/delta artifacts to versioned object storage with retention and mirrors.
  • Generate appcasts in CI and serve static bytes from GCS/CDN.
  • Keep api.omi.me/v2/desktop/appcast.xml as a long-lived compatibility shim.
  • Add an independent pre-auth rescue manifest and static stable installer URL.
  • Add bounded Sparkle delta generation after beta cadence is controlled.
  • Document key rotation, feed signing migration, withdrawal, and roll-forward recovery.

Legacy fallback removal criteria

Do not remove the legacy GitHub scan merely because the pointer path shipped. Remove it only after all are true:

  • At least 30 consecutive production days with no organic legacy_github fallback.
  • At least two successful beta promotions through the pointer path.
  • At least one explicitly approved stable promotion through the pointer path.
  • Zero unresolved pointer-vs-legacy semantic mismatches.
  • Pointer/LKG failure injection and kill-switch drills pass.
  • Dashboard and alert ownership are documented.
  • A removal PR deletes the first-page GitHub enumerator, legacy fallback metrics/flags after a deprecation window, and obsolete cache-clear behavior.

Injected tests and deliberate kill-switch drills must be labeled so they do not reset the organic-fallback clock.

Acceptance criteria

  • Stable and beta resolve only from explicit release IDs.
  • Missing/invalid stable fails closed and can never expose beta.
  • New pointer resolution is primary and all fallback use is visible with a reason.
  • Appcast HTTP 200 alone is insufficient; semantic probes verify expected channel contents and assets.
  • Beta exposure requires digest-bound qualification evidence.
  • Developers can run head-of-main without producing a beta or touching production bundles.
  • Update success means confirmed post-relaunch health, not pre-install intent.
  • The legacy fallback has objective, tracked removal criteria.
  • No stable pointer movement occurs as part of implementation without separate explicit production approval.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions