fix(cli): tolerate transient launchd startup state - #72
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Observed false failure
After
brew upgradeto FlickNote 1.1.0 on macOS,flicknote daemon installfailed immediately:Roughly two seconds later the daemon was healthy (service running, application ready, IPC protocol 4, PowerSync connected), and launchd showed one successful launch. Startup logs began about two seconds after the command's transient failure — the daemon was fine; the CLI gave up too early.
Root cause
wait_for_ready()inflicknote-cli/src/commands/daemon_lifecycle.rstreated the first transientServiceState::Stoppedobservation afterstart()as a terminal failure. launchd can report a freshly submitted/started user agent asstoppedwhile it is still being scheduled; the correct handling is to keep polling within the boundedSERVICE_OPERATION_TIMEOUTand only fail on timeout, exactly as already done for retryable IPC errors.The fix
wait_for_ready()now treatsStoppedas pending: it sleeps oneHEALTH_POLL_INTERVALand continues polling instead of returning an error. Nothing else changes:Runningand a compatible daemon IPC health check succeeding (the success path is unchanged and sits inside theRunningarm).NotInstalledduring readiness remains terminal.flicknote daemon status --verbose); platform details stay in logs/verbose diagnostics.sync.sockcompatibility aliases or cleanup (out of scope).Why unrelated foreground daemons still cannot satisfy readiness
While the service reports
Stopped,wait_for_ready()does not even consult the IPC health probe — it continues polling the managed service state. Success is only reachable from theRunningarm after a confirmedRunning+ healthy IPC double-check. A foreground daemon that answers IPC while the managed service staysStoppedis therefore never able to satisfy readiness; the command times out with guidance instead. The updated teststart_rejects_an_unrelated_healthy_daemon_when_the_service_does_not_runasserts this: it now expects the timeout error and asserts the health probe was never polled (health.polls() == 0).Deterministic reproduction
New test
install_tolerates_transient_stopped_and_retryable_ipc_failure_before_readinessdrives the productionLifecycleController::install_and_wait()orchestration with a fake service manager that reportsStoppedfor several polls beforeRunning, plus a retryable IPC failure before healthy IPC — it fails against the old code with the exact reported error (...service stopped before becoming ready...) and passes with the fix. Other new tests cover:NotInstalledremains terminal, protocol mismatch remains terminal, non-retryable IPC errors fail immediately; all pre-existing install/start/restart ordering tests still pass.Verification
All commands pass on this branch: