construct: auto-start the daemon from the TUI when none is running#398
Merged
Conversation
Now that `construct` is one binary that can also be the daemon, running the TUI with no daemon is a setup mistake rather than a user intent — so instead of erroring with a bare "connect <socket>" failure, the TUI spawns a detached `construct daemon run` in the background and waits for the socket. Opt out with CONSTRUCT_NO_AUTOSTART=1. Auto-start makes two `construct` launches able to race to start a daemon, which the old bind path handled badly: `server::serve` unconditionally unlinks the socket before binding, so a second daemon would *steal* a live socket and orphan the first's clients. Fix that race (and the same latent bug for a stray second `daemon run`) with a single-instance advisory lock: - The daemon takes an exclusive `flock` keyed to its socket path (`<socket>.lock`) before binding. The loser exits cleanly instead of stealing the socket. Held for the process lifetime; released across the restart exec() (CLOEXEC fd) and re-acquired by the new image. Daemons on different sockets don't contend. - `spawn_detached_daemon` (in the agentd lib) launches `<current exe> daemon run [--socket …]` in a new session (setsid) with logs to state_dir/daemon.log, so it survives the TUI's exit / terminal SIGHUP. - The TUI's `ensure_daemon_running` probes the socket, spawns if dead, and polls ~5s for readiness. Best-effort: on failure the normal connect error still surfaces. One-shot control subcommands (list/ping/…) intentionally do NOT auto-start — only the interactive TUI does. Updates spec 0026, README. Verified: TUI with no daemon auto-starts a detached daemon that binds the socket and survives the TUI exit; a second daemon on the same socket refuses and exits 0 while the first keeps serving.
edwin-zvs
added a commit
that referenced
this pull request
Jun 8, 2026
The daemon has had no unique role since #396/#398: `construct daemon run` runs it and the TUI auto-starts one. `constructd` was just a thin alias, so drop it — the daemon crate (`agentd`) is now library-only and the daemon ships solely inside `construct`. - crates/daemon: remove the `[[bin]] constructd` target and delete the `main.rs` shim; the crate is lib-only now. - e2e harness: spawn `construct daemon run` (and copy `construct` as the relocatable daemon binary) instead of `constructd run`. - scripts/test_agentd.sh, scripts/smoke.sh: launch via `construct daemon run`. - release.yml + install.sh: drop `constructd` from BINS (and `-p agentd` from the release build). The release tarball KEEPS its historical `constructd-<target>` asset name — already-released install.sh / `construct upgrade` builds fetch that exact name, so renaming it would break the download URL. - Docs/spec/comments updated; spec 0026 records that there is no standalone daemon binary and notes the one-time upgrade caveat below. Upgrade caveat (one-time): an `install.sh` baked into an *older* binary lists `constructd` in its `BINS` and aborts when it's absent from the new tarball, so `construct upgrade` across this single version hop fails — re-run the install one-liner instead. New installs and subsequent upgrades are unaffected. Verified: `cargo build --workspace` produces no `constructd`; `cargo test -p agentd` (66) and the e2e restart suite (3, incl. `restart_reloads_updated_binary`) pass with the daemon spawned as `construct daemon run`.
Merged
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.
Problem
After the binary consolidation (#396), running
constructwith no daemon up just errors:Now that one binary is both client and daemon, that's a setup mistake, not user intent — the TUI should just start one.
What
construct(the TUI) now auto-starts a daemon in the background when none is listening, then attaches. Opt out withCONSTRUCT_NO_AUTOSTART=1.This was the explicit non-goal in
specs/0026because a naive version is unsafe:server::serveunconditionallyremove_files the socket before binding (server.rs:30), so two racing daemons would steal each other's socket and orphan clients. This PR adds auto-start and fixes that race.How
crates/daemon/src/lib.rs): the daemon takes an exclusiveflockkeyed to its socket path (<socket>.lock) before binding. The loser exits cleanly instead of stealing the socket. Held for process lifetime; released across the restartexec()(CLOEXEC fd) and re-acquired by the new image. Daemons on different sockets don't contend. This also fixes the same latent bug for a stray secondconstruct daemon run.spawn_detached_daemon(agentd lib): launches<current exe> daemon run [--socket …]in a new session (setsid), stdio →state_dir/daemon.log, so it survives the TUI's exit / terminal SIGHUP.ensure_daemon_running(crates/cli): probes the socket, spawns if dead, polls ~5s for readiness. Best-effort — on failure the original connect error still surfaces.list/ping/…) intentionally do not auto-start; only the interactive TUI does (a scripted one-shot should fail fast, not leave a lingering daemon).specs/0026(auto-start moves from non-goal → decision, with the lock) and the README quick-start.Verification
cargo build(full workspace) ✓ ·cargo test -p agentd→ 66 passed ✓construct daemon run, which bound the socket, wrotedaemon.log, answeredping, and survived the TUI being killed ✓🤖 Generated with Claude Code