Skip to content

fix(native): recover unattended sync helper setup - #681

Open
Farhan (farhankhalaf) wants to merge 2 commits into
mainfrom
fix/helper-recovery
Open

Farhan (farhankhalaf) wants to merge 2 commits into
mainfrom
fix/helper-recovery

Conversation

@farhankhalaf

@farhankhalaf Farhan (farhankhalaf) commented Sep 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Builds on main's explicit-Enable registration policy. #682 is stacked on this branch and will be restacked after this lands. Linear: ENG-1447.

Helper registration can look like a persistent failure while macOS is still settling, or lose a newer failure during concurrent checks. Approval, setup progress, active-sync waits, manual remediation and failure now have distinct phases and matching actions, and removal has its own copy. Retry repeats the stored decision: a stored Enable retries as an explicit Enable, a stored Disable retries the removal, and an unset decision is handled like an observation. Observation never registers a missing helper. Busy observations preserve the last meaningful status, and a report from before a newer action is skipped rather than published.

Registration errors keep their diagnostic detail, including when macOS reports Enabled but helper verification then fails. Helper action messages follow the live permission row, and Retry failures from the panel and the repair banner report through the HELPER_E001 diagnostic. Bundle validation is main's validate_bundled_definition, unchanged. A small cleanup removes a redundant .nixmacignore binding so strict Clippy passes.

The change is two commits: the backend (7ae7c85) and the UI (4e5f802). They ship together because the backend alone would show "Try again" copy with no Retry button.

Test Plan

  • On macOS at the current head: cargo test -p nixmac 1,130 passed, 10 ignored; nixmac-helper 77 and nixmac-sync-agent 49 passed. Strict workspace Clippy with all targets and codegen and the rustfmt check passed.
  • TypeScript typecheck passed, including at the backend commit alone. Permissions and repair suites: 53 passed. The full frontend unit suite passes except 3 system-defaults-cta tests that fail identically on main under local Node 25 (localStorage.clear is not a function).
  • Regenerated oRPC and Specta bindings are byte-identical.
  • Signed-app real-helper smoke on macOS not rerun on this head. The pre-rebase branch passed it, but this head changes the registration policy, Retry routing and removal states, so that result does not carry over.
  • CI on this head.

Docs

  • No docs update needed; this repairs existing helper setup and recovery behavior.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GziRseGZMsJ5Yf7EboZLMc

@darkmatter

darkmatter Bot commented Sep 6, 2026 •

Copy link
Copy Markdown
Contributor

🎨 Storybook preview

Open Storybook preview

Updated for 4e5f802


⚠️ Detected UI changes (5)

These stories' HTML snapshots changed. I've added screenshots + links to the changed stories below. Review them carefully then accept the changes to regenerate baselines and include them in this PR:

Flows/Evolve › Playground

Flows/Evolve › Playground

Flows/Evolve › 1. Begin (idle)

Flows/Evolve › 1. Begin (idle)

Flows/Evolve › 2. Evolving (progress)

Flows/Evolve › 2. Evolving (progress)

Flows/Evolve › Evolving With Error Event

Flows/Evolve › Evolving With Error Event

Flows/Evolve › 3. Review (changes generated)

Flows/Evolve › 3. Review (changes generated)


Accept UI changes

  • Click here to accept these changes

Alternatively, you can run bun run test:update-snapshots locally to re-generate the baselines and then push the changes to this PR.

What does this do?

The screenshots above show UI changes detected by the Storybook
snapshot tests run on this PR. Each image is the rendered output of
a Storybook story from the code in this PR branch; the snapshot
test compared it against the committed baseline in
__snapshots__/ and flagged the difference.

Checking the box tells the darkmatter[bot] to regenerate the
baselines from this PR's current code and commit them directly to
this branch. The new baselines become the source of truth for
future runs — only accept after confirming the visual changes are
intentional.

Comparison baseline: the committed __snapshots__/ files on this
PR branch (carried forward from develop). Accept updates them in
place on this branch.

@github-actions

github-actions Bot commented Sep 6, 2026 •

Copy link
Copy Markdown
Contributor
Warnings
⚠️ ❗ Big PR (2862 lines changed). Consider splitting it into smaller, focused changes.
Messages
📖 No docs update needed — acknowledged.

📋 PR Overview

Lines changed 2862 (+2544 / -318)
Files 0 added, 18 modified, 0 deleted
Draft / WIP no
Has Test Plan yes
Linear issue yes
No Test Plan Needed no
New UI components no
New Storybook stories no
New Rust modules no
New TS source files no
New tests no
package.json touched no
Cargo.toml touched no
Infra / CI touched no

🔬 Coverage

Report Lines Statements Functions Branches
apps/native/coverage/coverage-summary.json 35.5% 34.9% 30.4% 30.5%

Generated by 🚫 dangerJS against 4e5f802

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 11, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-10T04:32:01.951158Z 4e5f802 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@prelint

prelint Bot commented Sep 11, 2026 •

Copy link
Copy Markdown

Ship with changes Typed helper lifecycle phases replace grace-period timer and text parsing

Product decisions in this change

Agree 1. The backend now owns a typed helper lifecycle with seven named phases (Ready, ApprovalRequired, Reconciling, WaitingForActivation, NeedsUserAction, Failed, Disabled), replacing UI-side parsing of diagnostic prose.

Every surface -- repair banner, permissions panel, feedback snapshot -- previously had to interpret the same diagnostic string independently. Centralising the phase in the backend means the same state drives the same copy and actions everywhere, and copy changes happen in one place. This is the correct ownership boundary: the backend understands what each reconciliation outcome means; the frontend should not re-derive that meaning.

Agree 2. The helper banner now appears immediately when the user's opted-in helper is not ready, with a neutral blue spinner for `reconciling` and `waitingForActivation` phases and amber for failure phases.

The old behaviour showed nothing for ten seconds, leaving the user with no signal while the backend was actively working. The new behaviour shows a neutral progress state from the first detection. Users who opted in and see a spinner understand that something is happening. Users who see amber understand that action may be needed. The distinction between neutral and alarming colour is appropriate: a helper that is registering is not a problem yet; a helper that has failed after the display window is.

Agree with concerns 3. A helper that keeps failing transitions from the neutral `reconciling` state to the `failed` state after ten seconds of failing attempts, not immediately.

Showing reconciling first and failed after a window is the right shape: it suppresses transient failures that self-resolve before the user reads the banner. The concern is that ten seconds remains a constant chosen without production timing data. The user experience is meaningfully better than before -- the user sees a spinner rather than nothing -- but the ten-second threshold could still be wrong in either direction. A fast machine that always fails in under one second waits unnecessarily. A slow registration that normally takes twelve seconds would show failed before it completes.

Option What users see Cost Effort to change later
Current (10 s fixed) Spinner, then failure May be too long on fast machines; too short on slow ones Change one backend constant
P90-derived threshold Spinner stops near the true tail Requires timing instrumentation on real hardware Needs data collection before the constant can change
Immediate failure No spinner; failure from first attempt Alarming on healthy installs that just take time Low effort but wrong UX

Agree 4. Retry is a separate action from Enable (Grant). Retry re-executes the stored decision without changing it or opening System Settings. Enable changes the user's opt-in.

A user who opted in and whose helper failed does not need to re-confirm the opt-in to try again. Conflating Retry with Enable would either force a redundant confirmation or silently turn a failed opt-out into a new opt-in. The separation is also correct for the failed-disable case: the permissions panel shows Retry for a failed removal without showing Enable, so the user cannot accidentally re-enable by clicking the recovery button.

Agree 5. Dismissing a helper banner clears when the phase changes, not permanently. A later failure re-shows the banner even if the user dismissed an earlier progress notice.

Permanent dismissal was the wrong model: a user who dismissed a reconciling banner should still see failed if recovery does not happen. Keying the dismiss state to the phase makes the dismiss mean "I see this progress state; do not repeat it" rather than "I never want to hear about the helper again." The phase is the identity that matters, not the banner kind.

Agree 6. After a Disable action completes, the permissions panel reads the live row, not the RPC response, to decide what to show.

The RPC response described the state at the moment the action ran. The row reflects the state after reconciliation and publication. Showing the RPC response as a notice could outlive the state it described: if activation was running when Disable was called, the response might say "waiting for activation" long after activation finished and the helper was removed. The live row is always current; the RPC result is a snapshot from one moment.

Agree 7. A `notFound` response from ServiceManagement no longer automatically means the installation is broken. The app checks the bundled plist and binary first, and registers if they are valid.

macOS's notFound is ambiguous: it means either "this service was never registered" or "the plist this service would use is gone." A valid app on a first install is in the first state, not the second. Treating both as a broken installation blocked first-time users from enabling the helper at all -- an Enable button was never shown. Checking the bundle independently resolves the ambiguity without relying on an unstable framework error code.

Agree 8. When a concurrent background check cannot get the reconciliation slot, the last meaningful row is preserved rather than replaced by a generic `reconciling` state.

A Busy result says only that this observation raced and lost. It carries no information about actual helper state. Replacing an actionable instruction such as "approve in Login Items" with a generic spinner would suppress information the user needs. Preserving the last row means the UI shows the most recent real observation until a real observation replaces it.

Agree with concerns 9. WaitingForActivation shows no user action -- no Retry, no Open Settings. The banner displays a spinner and the backend loops without a bounded wait.

Showing no action while an activation is legitimately in progress is correct: a registration attempt mid-activation would conflict with the running activation. The concern -- unchanged from the prior review -- is that the loop's WaitingOnActivation path does not increment the mutation counter and has no wall-clock timeout. If a darwin-rebuild activation hangs -- for example, an osascript call that blocks waiting for a UI prompt -- the user sees a spinner indefinitely. Dismissing the banner hides it; because dismissal clears on phase change and the phase does not change during a hang, the banner does not return after dismissal. The user is left with a stuck helper and no in-app signal or recovery path. This requires either a bounded wait in the helper loop or a confirmed statement that the activation layer has its own timeout that bounds this phase.

Agree 10. The Login Items instruction changes from "Approve nixmac" to "Allow nixmac".

The macOS Login Items & Extensions UI uses the word "Allow", not "Approve". Matching the system vocabulary reduces the chance that a user searches for a button that does not exist.

Agree 11. Helper failure diagnostics are included in the opt-in feedback snapshot, redacted of credential-shaped strings.

The failure detail (for example, SMAppServiceErrorDomain 1) is exactly the information the team needs to reproduce and fix real helper failures. The redaction test confirms the pattern removes token-shaped strings before the snapshot is sent. This is the correct tradeoff: diagnostic value with credential safety.

Since the last review

  • Addressed in code: Ten-second neutral window before failure escalation is a constant chosen without production timing data. (The frontend HELPER_GRACE_MS = 10_000 timer is removed. The backend FAILURE_PRESENTATION_AFTER: Duration = Duration::from_secs(10) replaces it. The user now sees a reconciling spinner during this window rather than nothing, which lowers the UX cost of an overly long threshold. The constant is still hardcoded but the stakes are lower.)
  • Still open: WaitingForActivation loop does not increment the mutation counter, so a hung activation leaves the user with an indefinite spinner and no in-app exit. (The drive loop still does wait(WAITING_INTERVAL); continue; on Reconciled::WaitingOnActivation(_) without incrementing working or checking elapsed time. The new test offers no repair action while waiting for an activation confirms no action is offered, and no timeout is added to this path.)
  • Addressed in code: Retry button copy does not distinguish a failed enable from a failed disable, so the user cannot confirm what Retry will do. (The repair banner condition gates on helperPreference === "granted", so "Couldn't finish enabling unattended sync" is contextually correct there. In the permissions panel, a failed disable shows Retry without an Enable button (confirmed by the test retries a failed removal without offering to enable the helper again), preventing accidental re-opt-in even though the copy is generic.)
  • Still open: No p90 reconciliation timing data to validate the ten-second neutral window on real hardware. (No timing data or instrumentation is added in this PR. The FAILURE_PRESENTATION_AFTER constant remains Duration::from_secs(10) with no comment citing measurement.)
  • Still open: Unknown whether the darwin-rebuild activation layer has its own timeout bounding the WaitingForActivation phase. (No timeout is added to the WaitingOnActivation loop path, and the PR description and diff contain no statement about whether the activation layer provides its own bound.)

Open questions

  • What is the p90 reconciliation time on a healthy first install on real hardware? This determines whether the ten-second failure presentation window correctly separates transient failures from persistent ones, or whether it is too short for slow machines and too long for fast ones.

  • Does the darwin-rebuild activation layer have its own wall-clock timeout that bounds the WaitingForActivation phase? If it does not, a hung activation leaves the user with an indefinite spinner and no in-app recovery after dismissal.

Recommendation

Ship with changes
The PR fixes a real first-install defect, replaces fragile UI-side text parsing with backend-owned typed phases, and separates Retry from Enable correctly. One prior concern remains fully open: the WaitingForActivation loop has no bounded timeout, and after a user dismisses the banner during a hung activation, no in-app signal or recovery returns. This is pre-existing but the PR explicitly tests that no action is offered during this phase without adding a bound, making it a confirmed design choice that needs a clear answer -- either a timeout in the loop or evidence that the activation layer already bounds this phase -- before this behavior is treated as finished.

@prelint prelint Bot added the ship it label Sep 11, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9a9974b350

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "Codex (@codex) review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "Codex (@codex) address that feedback".

@farhankhalaf

Copy link
Copy Markdown
Contributor Author

Follow-up on the activation-wait and timing questions:

  • The activation executors and the wait-until-finished behavior are unchanged by fix(native): recover unattended sync helper setup #681. The helper client has a 30-minute reply timeout, but that reports an unknown result; it does not terminate the detached activation. The shared lock continues to prevent overlapping system changes. Restarting nixmac does not stop that runner. The existing recovery boundary for a truly hung activation is a reboot, after inspecting the build's Console output. Adding safe cancellation or an execution deadline belongs in the activation layer, outside this presentation change.
  • The 10-second threshold was already present as HELPER_GRACE_MS. This PR moves that display policy into shared state and shows neutral progress immediately. It is not an execution timeout or a measured p90. The native smoke evidence does not establish a representative p90, and this PR does not claim one.
  • One title introduced here was too narrow: a running activation can come from a build or restore, not only unattended sync. Fixed in 9383d71: the banner now says “Waiting for the current build or restore to finish.” All 23 existing repair tests pass.

Source: helper wait behavior, client reply timeout.

@farhankhalaf

Copy link
Copy Markdown
Contributor Author

Follow-up is blocked only on #682's current Danger run, which is queued with no ARC runner assigned. The previous metadata run waited 20 minutes before the status edit superseded it. CI maintainers need to inspect ARC capacity/dispatch, get the current run executed, and check its coverage and Danger results. Validation remains incomplete until then.

Heads are unchanged: #681 9383d7139b6b50bc7fa0f27bd55e55965fc26708; #682 6cda456b9dc420a267166f715206b056c88f00d5.

  • Both #681 build and #682 build passed Linux/macOS Rust tests, strict Clippy, frontend unit tests (410/460), signing, notarization and portability. Evaluate passed on both. #681's metadata-triggered Danger run passed.
  • Storybook workflows completed, but both retain 24 inherited Evolve snapshot failures and the lucide-react error generating baseline images. The five-image galleries contain only after images. No snapshots were accepted.
  • Codex completed on both heads without new findings. Prelint's feat(native): add menu-bar popover and background attention #682 code review skipped unchanged files; its neutral product-review result means the review limit was reached. Danger's thin-router test warning has existing lifecycle and IPC behavior coverage. No new scoped regression was found.

Both CI checklists are updated, with #682 explicitly incomplete and native evidence limits preserved. No source edits, pushes or merges. Human review remains requested from czxtm, scottmcmaster and arximboldi.

@greptile-apps

greptile-apps Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

[Medium impact] Recovers unattended sync helper setup with new retry action.

Fix stale helper-row publication and satisfy the shared-error requirement before merging.

Findings

  1. P1 Old status hides newer failure ▶
  2. P2 Retry bypasses shared diagnostics ▶

Summary

Repairs first-time helper registration and adds Retry without changing the saved Enable/Disable choice.

  • Separates approval, setup, active-work waits, manual action, and failure into typed states.
  • Keeps helper action guidance in the live permission row and adds failure details to opted-in feedback.
  • The concurrency fix still lets an older loop publish Ready after a newer failure.
  • The new Retry error must use the required shared diagnostics.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Observe or user action] --> B[Read registration and saved choice]
  B --> C{No framework record?}
  C -->|Yes| D[Check bundled definition]
  C -->|No| E[Follow saved choice]
  D --> E
  E --> F{Result}
  F -->|Registered| G[Authenticate helper and check build]
  F -->|Approval or active work| H[Wait and check again]
  F -->|Failed| I[Show setup then recovery]
  G --> J[Publish typed permission row]
  H --> J
  I --> J
  J --> K[Permissions panel and repair banner]
Loading

Reviews (1) · Last reviewed commit: "fix(native): clarify active build helper..." · Reviewed by Greptile

Comment on lines 535 to 538
if nothing_more_to_do(&report) {
presentation.note_settled_report(&report, attempt_generation);
publish(&report);
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Old status hides newer failure

The generation check protects the saved failure, but not the row that drive_with_clock publishes. If the loop gets AtThisBuild, then a newer Disable attempt fails before publication, the loop still publishes the old Ready row and exits. The newer action cannot start another loop while CONVERGING is set. This hides the failed removal and can stop automatic recovery.

Skip stale reports before publishing or exiting, and ensure the newer attempt gets another pass. Update the race test to check the actual published row rather than a separate synthetic failure.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 7ae7c85. A report whose generation changed during the pass is skipped, and the loop observes again instead of exiting. After a settled publish it exits only if the generation is unchanged, and if an action starts while a row is being published the loop re-observes right away. a_stale_ready_row_never_hides_a_newer_failed_action asserts on the published rows (Stopped, then Removed).

Comment on lines +198 to +201
setNotice({
tone: "error",
message: `Retrying the unattended sync helper failed: ${error instanceof Error ? error.message : String(error)}`,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Retry bypasses shared diagnostics

The new Retry error is defined directly in PermissionsPanel. The repository requires user-facing TypeScript errors to be defined and reported through nostics in errors.ts, with a diagnostic code, title, and suggestion. Add the Retry diagnostic there and use it here instead of the ad hoc message. This repository requirement must be satisfied before merging.

Context Used: TypeScript error handling with nostics in the native app (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 4e5f802. Retry failures from this panel and from the repair banner now report through nostics as HELPER_E001 in errors.ts, with its explanation and fix.

@czxtm cooper (czxtm) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Requesting changes: this needs a rebase onto main and a decision on registration policy, because #691 (f28a2e2, "Register valid helpers on explicit Enable") already landed an incompatible fix for the same notFound problem and the branch conflicts with it in 5 files (protocol.rs, reconcile.rs, service.rs, permissions-panel.tsx and its test).

Blocking

  1. Registration policy. converge registers on (NotRegistered | NotFound, Goal::Install) with no distinction between a background status check and a user click (reconcile.rs#L531-L534). With a saved Enable preference, any observation or permissions refresh can call register without a click. Main splits this with enum Invocation, and Invocation::Observe returns Reconciled::RegistrationUnavailable and never registers. Please rebase, adopt main's explicit-Enable policy unless you want to make the case for background registration, and route the new helperRetry through the Enable invocation when the saved decision is Enable. The tests that expect background re-registration should go.
  2. Bundle validation. check_service_definition (service.rs#L68-L115) reads with fs::read and fs::metadata, which follow symlinks, and it rejects Program but not ProgramArguments. The helper runs as root, so a bundle whose plist or binary resolves outside the bundle, or which carries ProgramArguments, passes this check. Drop it and keep main's validate_bundled_definition, which canonicalizes, requires regular files inside the bundle, and rejects both keys. If you want an Unavailable versus Invalid split, layer it on top of that.

Two Greptile threads are still open (the stale-Ready publish race in drive_with_clock, and the Retry diagnostics one). The race is a merge blocker for me too, and the fix needs a regression test that asserts on the published row.

Smaller things

  • After the rebase, phase_with_failure_visibility and the debug_assert! in drive_with_clock need to handle Reconciled::RegistrationUnavailable explicitly, with a phase that offers Enable or Retry. Otherwise it can fail to compile or fall into a wildcard arm.
  • Danger flags no Linear ID, and the PR bundles the registration fix, typed lifecycle, Retry UI, diagnostics and a Clippy cleanup. Once the validation piece is dropped, splitting the backend phase and retry from the UI and diagnostics would help, since #682 is stacked on this.
  • #682 will need restacking after this lands, and the test results were produced before the rebase onto #691, so please refresh them on the new head.

Farhan (farhankhalaf) and others added 2 commits November 9, 2026 20:01
…current

Port the helper recovery backend onto the explicit-Enable registration policy.
Observation still never registers a missing helper.

- Add a Retry action that carries out the stored decision. A stored Enable
  retries as an explicit Enable, so a missing registration gets one new
  register. A stored Disable retries the removal. An undecided user is handled
  exactly like an observation: Retry never registers a missing helper, though
  it adopts an existing registration the same way an observation does. Retry
  never opens System Settings.
- After a register error, judge the result from one fresh status read
  (approval, enabled and verified, or still missing) and keep the register
  error in the report instead of issuing a second register.
- Report a typed HelperPermissionPhase on the permission row and on helper
  action results. RegistrationUnavailable maps to Failed, which offers Retry.
  While a stored Disable is in progress, waiting on an activation or failed,
  the row describes the removal instead of setup.
- Classify every report into an exhaustive loop step instead of relying on a
  debug assertion.
- Skip a convergence report that predates a newer user action, so a stale
  Ready row can no longer hide a failed Disable. A start request that arrives
  while the loop is ending is recorded and served, and each pass acknowledges
  the requests it serves, so no request is dropped and none buys a second
  attempt budget.
- Busy observations preserve the last meaningful row, and the last helper
  failure is included in the opt-in feedback snapshot.
- Regenerate the oRPC and specta TypeScript bindings, and give the existing
  panel test's HelperReport the new required phase so this commit typechecks
  on its own.
- Remove a redundant `.nixmacignore` binding that strict Clippy rejects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…backend

- The permissions row and the repair banner read the typed helper phase:
  approval, setup progress, an active build or restore, a needed user action,
  removal and failure each get their own copy and action.
- A failed phase, including a missing registration, offers Retry, which calls
  helperRetry and keeps the saved Enable or Disable decision. Disable stays
  available for a wanted helper. No Enable is offered while a stored Disable
  has not finished; a failed removal offers Retry.
- Helper action notices follow the live row instead of copying an earlier
  request result. While an Enable replaces the previous row, the banner
  shows setup progress rather than a failure.
- A failed Retry from the panel or the repair banner is reported through the
  HELPER_E001 nostics diagnostic. The banner clears that error when its
  failure episode ends, and ignores a Retry that settles after its episode
  ended.

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

Copy link
Copy Markdown
Contributor Author

Rebuilt on main (1492663) as two commits: the backend (7ae7c85) and the UI (4e5f802).

  • Registration policy: adopted main's explicit-Enable policy. Observation returns RegistrationUnavailable and never registers. helperRetry reads the stored decision after taking the slot: a stored Enable runs Invocation::Enable and a stored Disable retries the removal. An unset decision is handled exactly like an observation, so Retry never registers a missing helper, though it can still adopt an existing registration the same way an observation does. Retry never opens System Settings. The tests that expected background re-registration are gone.
  • Bundle validation: check_service_definition and ServiceDefinitionError are dropped; only main's validate_bundled_definition remains.
  • Stale Ready race: drive_with_clock skips a report whose generation changed during the pass and observes again instead of exiting. If an action starts while a row is being published, the loop re-observes right away. a_stale_ready_row_never_hides_a_newer_failed_action asserts on the published rows. A start request that arrives while the loop is ending is recorded and served, and each pass acknowledges the requests it serves.
  • RegistrationUnavailable: mapped explicitly to the Failed phase, which offers Retry. The debug_assert! chain is now an exhaustive match, so a new report variant does not compile until it is classified.
  • Removal: while a stored Disable is in progress, waiting on an activation, or failed, the row uses removal wording and offers no Enable; a failed removal offers Retry.
  • Retry diagnostics: failures from the panel and the repair banner report through nostics as HELPER_E001.
  • Split: kept as one PR with the backend and UI as separate commits. Merging the backend alone would leave main showing "Try again" copy with no Retry button.
  • Linear: ENG-1447.

Local test results on the new head are in the description. The signed-app helper smoke on a real Mac has not been rerun on this head. #682 will be restacked once this lands.

@sonarqubecloud

Copy link
Copy Markdown

This branch was successfully deployed

1 active deployment
storybook-preview — 4e5f8029 Deployed Nov 10, 2026 by github-actions[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants