Skip to content

construct: auto-start the daemon from the TUI when none is running#398

Merged
edwin-zvs merged 1 commit into
mainfrom
construct-autostart-daemon
Jun 8, 2026
Merged

construct: auto-start the daemon from the TUI when none is running#398
edwin-zvs merged 1 commit into
mainfrom
construct-autostart-daemon

Conversation

@edwin-zvs

Copy link
Copy Markdown
Contributor

Problem

After the binary consolidation (#396), running construct with no daemon up just errors:

Error: connect /Users/moon/.local/state/construct/construct.sock

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 with CONSTRUCT_NO_AUTOSTART=1.

This was the explicit non-goal in specs/0026 because a naive version is unsafe: server::serve unconditionally remove_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

  • Single-instance lock (crates/daemon/src/lib.rs): 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 process lifetime; released across the restart exec() (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 second construct 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.
  • One-shot control subcommands (list/ping/…) intentionally do not auto-start; only the interactive TUI does (a scripted one-shot should fail fast, not leave a lingering daemon).
  • Updates 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 ✓
  • Auto-start: drove the TUI under a PTY with no daemon → it spawned a detached construct daemon run, which bound the socket, wrote daemon.log, answered ping, and survived the TUI being killed
  • Race safety: started a second daemon on the same socket → it logged "another construct daemon already owns this socket; exiting", exited 0, and the first daemon kept serving (no steal) ✓

🤖 Generated with Claude Code

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
edwin-zvs merged commit f53eae5 into main Jun 8, 2026
1 check passed
@edwin-zvs
edwin-zvs deleted the construct-autostart-daemon branch June 8, 2026 01:48
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`.
@edwin-zvs edwin-zvs mentioned this pull request Jun 16, 2026
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