fix(desktop): resolve repos_dir symlink at boot before agent restore#1231
Merged
Conversation
repos_dir lived only in frontend localStorage; the backend learned it solely via apply_workspace, which runs after React mounts — later than the async restore_managed_agents_on_launch. An agent could clone into the empty real REPOS dir before the symlink was ever applied, and once REPOS is non-empty ensure_repos_symlink correctly refuses forever, silently breaking a configured repos_dir on every boot. Persist repos_dir to a ~/.buzz/.repos-dir dotfile on apply_workspace and read it in the setup hook to resolve the REPOS symlink before agents are restored, making the backend authoritative at boot. The frontend path stays as the live-edit bridge. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The boot symlink resolve logged-and-continued on Err. If a configured repos_dir's external target was transiently unavailable at launch (NAS not mounted, FileVault unlock lag), REPOS stayed the empty real dir ensure_nest provisioned and the spawned agent restore could clone into it — permanently re-triggering the race the persisted dotfile exists to close. Gate the restore spawn on the symlink outcome: resolve_repos_at_boot fails closed, skipping restore for the launch only when a repos_dir was configured AND its symlink could not be resolved. The no-repos_dir path (real in-nest default) and the happy path are unchanged. A skipped restore recovers on the next reachable boot; a misplaced clone does not. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
A bad persisted repos_dir bricked the app: apply_workspace hard-rejected on boot, parking the FE forever on the loading gate with no way to edit the value. The reject only ever fired on boot (apply_workspace's sole caller is useWorkspaceInit), so it guarded a dialog-apply path that never existed. Make a bad repos_dir non-fatal on apply, mirroring the existing symlink- failure branch: relay/keys still apply (the relay is the active workspace's own choice, orthogonal to the filesystem repos dir), the bad value is NOT persisted (clearing .repos-dir so the next boot resolves cleanly and agent restore proceeds — persisting it would silently skip restore every boot), the REPOS symlink is skipped, and a repos-dir-error toast surfaces the reason. The app boots into a working state where the user fixes the value in settings. Add a validate_repos_dir command so Add/Edit dialogs block Save on a bad path (the front door); the backend fallback is the safety net for a path that goes bad after save. Add a positive boot log line so a healthy resolve is observable, not just the failure branches. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The bad-repos_dir recovery fix hinged on a persisted-state guarantee that no test asserted: a bad path must CLEAR .repos-dir so the next boot's should_restore_agents(false, _) restores agents. apply_workspace takes State/AppHandle and has no unit-test seam, so extract the persist decision into a pure effective_repos_dir(nest, candidate) helper and assert all three cases drive the correct dotfile state (bad -> cleared, valid -> raw trimmed path, empty/none -> cleared). Resolving the nest is now soft so relay/keys apply unconditionally even when $HOME is unset. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…est helper The conn_manager.register() signature has required a grace_limit: u8 argument since #1226. #1227's register_presence_sub helper was added with only 5 args, breaking the buzz-relay test build (E0061). Matches the sibling register_conn helper's value of 3. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…ir binding The validateReposDir binding pushed tauri.ts one line past its existing 1198 override. No clean no-op trim exists — the file is uniformly formatted with no redundant blank lines — so the override moves to 1199 with the reason updated to cover the new binding. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
ac4ac2f to
cdcd393
Compare
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.
Follow-up to #1194, which added the configurable
repos_dirfeature. That PR left a startup-ordering race: an agent could clone into~/.buzz/REPOSbefore it was symlinked to the user's external repos directory, permanently blocking the symlink.The bug
repos_dirlived only in frontendlocalStorage. The Rust backend learned it solely throughapply_workspace, which runs after React mounts. Butrestore_managed_agents_on_launchis spawned earlier in the setup hook:ensure_nest()provisionsREPOSas an empty real directory at boot.restore_managed_agents_on_launchspawns and an agent clones into that empty realREPOS.ensure_repos_symlinknow sees a non-empty real directory and refuses — so the symlink is never created, permanently.The fix
Make the backend authoritative for
repos_dir:repos_dirto a~/.buzz/.repos-dirnest dotfile (mirrors the existing.nest-agents-versiondotfile precedent).apply_workspacewrites it.ensure_nest(), before the agent-restore spawn — soREPOSis a symlink before any agent can clone.Fail-closed gate
When a configured
repos_diris present and the boot symlink resolve fails — e.g. a transiently unavailable external target (unmounted volume, NAS lag, FileVault unlock delay) — agent restore is skipped for that launch rather than letting an agent clone into the wrongREPOS. A skipped restore is recoverable on the next launch; a wrong-place clone is permanent corruption. When norepos_diris configured, the real-directory default is correct and restore proceeds normally.Known boundaries
apply_workspacecompleting, boot uses the prior workspace's value. Self-corrects on the first frontend apply.repos-dir-errortoast gap: the fail-closed skip-restore branch produces a real boot-time error with no UI signal, because the toast listener registers asynchronously after mount. This applies to the skip-restore branch, not just the symlinkErr. Separable and tracked, not addressed here.Related: #1194