Skip to content

fix: root-fix helper "update available" nag for bundled owners (v1.43 W1)#382

Merged
JasonYeYuhe merged 1 commit into
mainfrom
fix/helper-bundled-owner-nag
Jul 23, 2026
Merged

fix: root-fix helper "update available" nag for bundled owners (v1.43 W1)#382
JasonYeYuhe merged 1 commit into
mainfrom
fix/helper-bundled-owner-nag

Conversation

@JasonYeYuhe

Copy link
Copy Markdown
Collaborator

W1 — structurally fix the helper "update available" nag (v1.43)

Root-fixes the perpetual, unclearable "Update available →1.30.0" nag that DEVID users see in Settings → Managed CLI Helper. Two helpers can own the local UDS socket: the Swift helper embedded in the app bundle (updates with the app) and the standalone .pkg Python helper. HelperInstaller.refresh() compared whichever owner's hello version against the published .pkg manifest — so a bundled owner was forever told to "update" a helper it can't update independently (installing the .pkg doesn't evict a bundled owner that owns the socket).

What changed (all additive on the wire)

  1. New additive hello field implementation"swift-bundled" (Swift helper) / "python-pkg" (Python helper). Older helpers omit it → decoded as nil → legacy path unchanged. Element-for-element mirror; the additive-only evolution rule is now written into Protocol.swift's head comment.
  2. Nag suppressionHelperInstaller gains a .bundled(version:) state; refresh() delegates to a pure, testable resolveState(...) that returns .bundled for a swift-bundled owner regardless of the manifest (and skips the manifest fetch for it). Every .pkg/older-helper path is preserved byte-for-byte.
  3. Controlled helper swap — after an in-place app update, ensureRegistered() is a no-op and the KeepAlive LaunchAgent keeps the old helper process alive until re-login. kickstartBundledHelperIfAppUpdated(...) detects an app-binary change via a persisted app-version sentinel and launchctl kickstart -ks our own LaunchAgent once per new app version, so the new bundle's helper takes over immediately. DEVID-only (#if DEVID_BUILD).
  4. UI owner splitCompanionCLISection renders .bundled as "built-in — updates with the app" and hides the .pkg-only Check for Updates / Uninstall… controls.

Why an app-version sentinel and not a helper-version compare (review-driven)

The first cut gated the swap on implementation == "swift-bundled" + a helper-version compare. Dual review (codex + agy) independently caught a P1: the existing v1.42 install base runs a bundled helper reporting 1.23.0 with no implementation field, so a field/version-gated swap can't identify it as stale — the nag would persist for exactly the users the fix targets, until re-login. The app-version sentinel sidesteps owner identification entirely: it kickstarts only our own LaunchAgent label (never the separately-labelled .pkg helper) once per new app version, which also covers a helper-binary change that keeps the same version number. This let us drop the app-side version mirror + the 3-way CI pin the first cut added.

Testing

  • Python: pytest helper/ — 950 passed (incl. implementation == "python-pkg" hello assertion).
  • HelperSwift: swift test — 624 tests (incl. "swift-bundled" hello assertion).
  • CLIPulseCore: swift test default + -Xswiftc -DDEVID_BUILD — 0 failures. New pure-logic tests for resolveState (bundled-never-nags regression pin, .pkg/older-helper legacy paths preserved, nil/socket/manifest branches) and shouldKickstartHelperForAppUpdate (changed / first-run / same-version / build-bump).
  • Regression-red proof: with the bundled short-circuit disabled, test_resolveState_bundledOwnerNeverNagsEvenWhenManifestNewer fails producing exactly the shipped bug (updateAvailable(1.29.0 → 1.30.0)), confirming the test guards it.
  • Compile matrix: xcodebuild app target — non-DEVID macOS, DEVID macOS, iOS, and MAS archive all build (the #if DEVID_BUILD launchctl exec never compiles into the sandboxed MAS build).
  • Review: two rounds of codex + agy + an adversarial multi-lens workflow audit. Round 1 caught the P1 above → sentinel redesign; a confirmed P3 dead pairing-hint removed (follow-up filed to mirror paired into the Swift hello). Round 2 caught a P2 post-swap UI race (an in-flight refresh() capturing the pre-swap helper could leave a stale nag) → fixed with a monotonic-epoch guard in refresh() (last-started wins, so a slow manifest fetch can't clobber a newer result) + refreshAfterHelperRespawn() that reconciles the UI once the kickstarted helper rebinds; plus P3 comment-accuracy fixes (bundled owners now bypass the .pkg compare). Final: agy SHIP, codex re-check clean.

Known transient

On the first launch after upgrading from a pre-v1.43 helper, there's a ~1–2s window (old helper still owner, before the kickstart's respawn binds) where the section could briefly show the old nag before settling to .bundled; it self-heals via the existing re-probe hook.

🤖 Generated with Claude Code

… W1)

The app-embedded Swift helper updates WITH the app, but HelperInstaller
compared its hello version against the standalone .pkg manifest — showing a
perpetual, unclearable "Update available" nag (installing the .pkg can't evict
a bundled owner that owns the socket).

- hello gains an additive `implementation` field ("swift-bundled"/"python-pkg";
  absent on older helpers → nil → legacy path). Additive-only wire rule is now
  written into Protocol.swift's head comment.
- HelperInstaller gains a `.bundled` state; a pure, testable resolveState()
  returns it for a swift-bundled owner REGARDLESS of the manifest. Every
  .pkg/older-helper path is preserved exactly.
- Controlled swap: an app-version sentinel `launchctl kickstart -k`s our OWN
  LaunchAgent once per app update so the new bundle's helper takes over now —
  clearing the nag even for users upgrading from a pre-v1.43 helper whose hello
  can't be identified as stale (dual-review P1). DEVID-only.
- Post-swap the UI is reconciled (refreshAfterHelperRespawn + a monotonic-epoch
  guard in refresh()) so an in-flight refresh can't leave a stale nag.
- CompanionCLISection renders .bundled as "built-in — updates with the app" and
  hides the .pkg-only Check-for-Updates/Uninstall.

Tests: pytest helper (950), swift test HelperSwift (624) + CLIPulseCore default
& -DDEVID_BUILD (0 failures) incl. resolveState regression pins + sentinel
truth table; regression-red proof; app compile matrix (non-DEVID/DEVID macOS,
iOS, MAS archive). Reviewed by codex + agy (2 rounds) + an adversarial workflow
audit; all findings addressed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@JasonYeYuhe
JasonYeYuhe merged commit 17e6e92 into main Jul 23, 2026
44 of 46 checks passed
@JasonYeYuhe
JasonYeYuhe deleted the fix/helper-bundled-owner-nag branch July 23, 2026 09:29
JasonYeYuhe added a commit that referenced this pull request Jul 23, 2026
…#383)

Version bump in prep for the v1.43.0 release (W1 nag fix #382 + merged
#377/#378/#380/#381). Release-prep only — no DEVID cut, no publish, no ASC.

- Apple MARKETING_VERSION 1.42.0 → 1.43.0, CURRENT_PROJECT_VERSION 95 → 96
  (all 10 target/config entries).
- Android versionName → 1.43.0, versionCode 61 → 62 — solely to satisfy
  check-versions.sh drift gate; NOT submitting to Play this cycle.

Verified: check-versions.sh OK; android ./gradlew testDebugUnitTest (JDK 17)
BUILD SUCCESSFUL.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant