Skip to content

fix(cli): install the engine on the daemon path without a browser wizard (waired#835 §11) - #117

Merged
gen16k merged 1 commit into
mainfrom
feat/835-daemon-path-inference
Jul 20, 2026
Merged

gen16k merged 1 commit into
mainfrom
feat/835-daemon-path-inference

Conversation

@gen16k

@gen16k gen16k commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

The bug this fixes exists today, on macOS

#115 gave the daemon path an executor-driven engine install, but gated it on setupActive — a browser wizard actually driving. I scoped that too narrowly. Every terminal-only daemon-path install still finishes with no engine: --non-interactive, --no-browser, no TTY, pressing Enter to take the terminal back, or simply not touching the browser.

And that is not a future problem waiting on the §11.2 ordering flip. macOS already takes the daemon path on its default install:

# packaging/install/install.sh  darwin_install
darwin_register_agent "$state_dir"     # :1150 — launchctl bootstrap
darwin_install_log_rotation            # :1151
darwin_write_control_url "$state_dir"  # :1152
darwin_maybe_init "$state_dir"         # :1153 ← init runs HERE

darwin_register_agent runs waired-agent install, whose plist sets RunAtLoad=true — service_darwin.go's own comment says "the agent starts the moment launchctl bootstrap finishes". So the daemon is up before init runs, daemonReachable is true, and init takes runInitViaDaemon. The installer also stopped pre-installing the engine (install.sh:1143-1148).

Net: the default macOS install has been completing with no engine, while darwin_next_steps prints installed by sign-in when local inference is on (sudo waired init) — the opposite of what happens.

installtest cannot see this. Every enroll leg forces the standalone branch — --bypass-mode, --google-sa-login, or an explicit service stop — so main.go:325's !*googleSALogin guard means the daemon path has never been exercised by installtest at all. That coverage gap is what hid it.

Evidence level: verified by reading the code path end to end. Not reproduced on a real macOS host — I don't have one here.

Changes

ensureDaemonPathEngine — install when the host wants inference, not when a wizard is driving. The condition is read from the daemon's own inference subsystem state rather than from any flag, so it reflects what the agent actually decided:

subsystem_state action why
no_engine install inference is on and there is nothing to serve with
disabled / stopped skip deliberate operator states
anything else skip an engine already exists
unreachable until engineWaitForStatus skip never install on a guess

installEngineAsExecutor — extracted from the wizard path so both entry points share one claim → decide → install → report core. Only the narration differs; saying "for the setup in your browser" during a terminal-only install is confusing.

SetupState now serves state_dir unconditionally. #115 served it only alongside a desired engine, with the comment "there is nothing to install otherwise". That reasoning is precisely what this PR falsifies — a no-wizard install has no desired engine and still needs the destination. Withholding it is what would keep the install engine-less.

applyDaemonInitInference — re-apply --inference-enabled, --share-with-mesh and --inference-bundled-model-id after a daemon-path login. LoginStartRequest carries only a control URL and a device name, so these three have been accepted and silently dropped; install.ps1:1372-1373 has been passing --inference-enabled on Windows to no effect (§11.2). Applied via the management routes that already own these controls — no new wire.

  • nil means "not passed", so absence never overwrites what the host already decided.
  • A model is not requested on a host that just turned inference off (that would be a multi-GB download nobody can use).
  • Every failure warns; login already succeeded and a knob that did not apply must not fail the install.

Ordering is load-bearing. Flags are re-applied before awaitBrowserSetup (nothing races desired_model_id, per §4.2) and before waitForBundledModel (the terminal must not wait on a download the operator asked to skip, nor download the auto-selected model and only then switch).

Tests

New (init_daemon_inference_test.go): a 6-case table over flag re-application including "nothing passed touches nothing" and "model skipped when inference turned off"; failure-tolerance; a 5-state table for the engine decision; the give-up path against an unreachable daemon; the no-wizard install happy path; and a 6-case skip table (disabled / stopped / engine present / no state dir / wizard already claimed / older daemon).

daemonPathEngineInstall takes goos/elevated as parameters per the repo's cross-OS rule, so the install arm is reachable from an unprivileged runner.

Updated: TestSetupStateOmitsStateDirWithoutDesiredEngine → TestSetupStatePublishesStateDirWithoutDesiredEngine. It pinned the assumption this PR overturns, and I rewrote its comment to say why rather than just flipping the assertion.

Verification

gofmt -l .          # clean
go vet ./...        # clean
golangci-lint run   # 0 issues
go test ./... -timeout 10m   # all pass
make verify-cross   # 4 GOOS/GOARCH combinations clean

Not verified: a real macOS install, and the actual engine download (a ~GB fetch behind an elevation gate). The installer seam is the same installOllama that has shipped on the interactive path.

Still uncovered, deliberately: installtest has no daemon-path enroll leg. Adding one needs a login mode that reaches runInitViaDaemon, which today means relaxing the !*googleSALogin guard or driving a browser login. That belongs with the ordering flip (PR-R2), where the daemon path becomes the default and the gap becomes urgent.

Refs

🤖 Generated with Claude Code

…ard (waired#835 §11)

waired#835 #115 gave the daemon path an executor-driven engine install,
but gated it on setupActive — a browser wizard actually driving. Every
terminal-only daemon-path install therefore still finishes with no
engine: --non-interactive, --no-browser, no TTY, pressing Enter to take
the terminal back, or simply not touching the browser.

This is not a future problem. macOS reaches the daemon path on its
DEFAULT install today:

    darwin_register_agent "$state_dir"   # install.sh:1150 - launchctl bootstrap
    darwin_maybe_init "$state_dir"       # install.sh:1153 - init runs here

The plist that registration installs has RunAtLoad=true (service_darwin.go:
"the agent starts the moment launchctl bootstrap finishes"), so the daemon
is up before init runs and daemonReachable is true. The installer also
stopped pre-installing the engine. So the default macOS install has been
completing with no engine while its closing banner says "installed by
sign-in when local inference is on".

installtest cannot see it: every enroll leg forces the standalone branch
via --bypass-mode, --google-sa-login, or an explicit service stop, so the
daemon path has never been exercised there at all.

Changes:

- ensureDaemonPathEngine: install when the HOST WANTS INFERENCE, not when
  a wizard is driving. The condition is read from the daemon's own
  inference subsystem state, so it reflects what the agent decided rather
  than what a flag claimed: only "no_engine" installs; "disabled" and
  "stopped" are deliberate operator states and anything else means an
  engine already exists. Bounded by engineWaitForStatus so a silent
  daemon cannot hang init, and it never installs on a guess.
- installEngineAsExecutor: extracted from the wizard path so both entry
  points share one claim-decide-install-report core, with only the
  narration differing (saying "for the setup in your browser" on a
  terminal-only install is confusing).
- SetupState now serves state_dir unconditionally. #115 served it only
  alongside a desired engine, reasoning there was nothing to install
  otherwise; that reasoning is what this commit falsifies. Withholding
  the destination is exactly what would keep a terminal-only install
  engine-less.
- applyDaemonInitInference: re-apply --inference-enabled,
  --share-with-mesh and --inference-bundled-model-id after a daemon-path
  login. LoginStartRequest carries only a control URL and a device name,
  so these were accepted and silently dropped — install.ps1 has been
  passing --inference-enabled on Windows to no effect (§11.2). Applied
  through the management routes that already own these three controls;
  no new wire. Nil means "not passed", so absence never overwrites what
  the host already decided, and a model is not requested on a host that
  just turned inference off. Every failure warns rather than failing a
  login that already succeeded.

Ordering is load-bearing: the flags are re-applied before
awaitBrowserSetup (so nothing races desired_model_id) and before
waitForBundledModel (so the terminal does not wait on a download the
operator asked to skip, or download the auto-selected model and only
then switch).

This lands before the §11.2 installer ordering flip, which puts Linux
and Windows onto the same path macOS is already on.

Refs: waired-ai/waired#835

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: gen16k <gen16k@gmail.com>
@gen16k
gen16k merged commit b060a4d into main Jul 20, 2026
13 checks passed
gen16k added a commit that referenced this pull request Jul 20, 2026
…takes the daemon path (waired#835 §11.2)

`waired init` attaches to a running local agent and drives the browser-based
first-run onboarding only when the daemon is already up (the daemon-path
guard at cmd/waired/main.go); otherwise it falls through to the legacy
standalone terminal enroll. Until now only macOS met that condition — its
installer registers a RunAtLoad LaunchDaemon (darwin_register_agent) before
darwin_maybe_init, so macOS init has always taken the daemon path. Linux and
Windows ran `waired init` BEFORE starting the service, so init took the
standalone path and the browser onboarding never engaged.

Flip the order so all three OSes start the agent first:

- install.sh: linux_apt_install and linux_apt_update call linux_service_up
  before linux_maybe_init.
- debian postinst: start the unit on a FRESH install (was enable-only), so a
  plain `apt install` followed by a manual `sudo waired init` also takes the
  daemon path. Upgrade behaviour is unchanged — the restart-on-upgrade stays
  gated on the captured enabled-state, so an operator-stopped unit is not
  revived.
- install.ps1: Ensure-AgentRunning moves ahead of Invoke-WairedInit, under
  the Background-service section.
- waired-setup.iss (GUI installer, which does not run init itself): add a
  fresh-install `waired-agent start` [Run] step so a later `waired init`
  finds the daemon up, and correct the stale "started by waired init" note.

Safe before sign-in: the daemon boots identity-less and idles until
enrolment (#177). When the daemon is not reachable (non-systemd container,
start failure) init falls back to the standalone path, so nothing regresses.

On Linux the one-liner still pre-installs the engine (linux_install_ollama),
so the daemon-path engine install (already merged, #117) is a no-op there;
it is exercised on the `apt install` + manual-init path and on macOS.

docs-site/getting-started/first-run.mdx already documents this behaviour
("a fresh install starts it first"); this brings the installers into line.

The new CI installtest leg that exercises the daemon path end-to-end is
deferred to a follow-up: it needs out-of-band CP login completion and edits
scripts/dev/lib/installtest-enroll.sh, which is under concurrent
modification in another session.

Refs waired-ai/waired#835

Signed-off-by: gen16k <gen16k@gmail.com>
gen16k added a commit that referenced this pull request Jul 20, 2026
…takes the daemon path (waired#835 §11.2) (#119)

`waired init` attaches to a running local agent and drives the browser-based
first-run onboarding only when the daemon is already up (the daemon-path
guard at cmd/waired/main.go); otherwise it falls through to the legacy
standalone terminal enroll. Until now only macOS met that condition — its
installer registers a RunAtLoad LaunchDaemon (darwin_register_agent) before
darwin_maybe_init, so macOS init has always taken the daemon path. Linux and
Windows ran `waired init` BEFORE starting the service, so init took the
standalone path and the browser onboarding never engaged.

Flip the order so all three OSes start the agent first:

- install.sh: linux_apt_install and linux_apt_update call linux_service_up
  before linux_maybe_init.
- debian postinst: start the unit on a FRESH install (was enable-only), so a
  plain `apt install` followed by a manual `sudo waired init` also takes the
  daemon path. Upgrade behaviour is unchanged — the restart-on-upgrade stays
  gated on the captured enabled-state, so an operator-stopped unit is not
  revived.
- install.ps1: Ensure-AgentRunning moves ahead of Invoke-WairedInit, under
  the Background-service section.
- waired-setup.iss (GUI installer, which does not run init itself): add a
  fresh-install `waired-agent start` [Run] step so a later `waired init`
  finds the daemon up, and correct the stale "started by waired init" note.

Safe before sign-in: the daemon boots identity-less and idles until
enrolment (#177). When the daemon is not reachable (non-systemd container,
start failure) init falls back to the standalone path, so nothing regresses.

On Linux the one-liner still pre-installs the engine (linux_install_ollama),
so the daemon-path engine install (already merged, #117) is a no-op there;
it is exercised on the `apt install` + manual-init path and on macOS.

docs-site/getting-started/first-run.mdx already documents this behaviour
("a fresh install starts it first"); this brings the installers into line.

The new CI installtest leg that exercises the daemon path end-to-end is
deferred to a follow-up: it needs out-of-band CP login completion and edits
scripts/dev/lib/installtest-enroll.sh, which is under concurrent
modification in another session.

Refs waired-ai/waired#835

Signed-off-by: gen16k <gen16k@gmail.com>
@gen16k
gen16k deleted the feat/835-daemon-path-inference branch August 3, 2026 19:15
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