Skip to content

Add OpenClaw as a desktop app and a coding agent - #10246

Merged
spencerbull merged 1 commit into
quattrofrom
add-openclaw-ai-app
Sep 5, 2026
Merged

Add OpenClaw as a desktop app and a coding agent#10246
spencerbull merged 1 commit into
quattrofrom
add-openclaw-ai-app

Conversation

@spencerbull

@spencerbull spencerbull commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Adds OpenClaw the same way Hermes landed in #7469: an Install > AI entry that sets it up as a desktop app, a Remove > AI counterpart, and a Setup > Defaults > Agent choice that installs it on first pick.

OpenClaw's desktop experience is its Control UI, served by a local gateway that runs as a systemd user unit. So "the app" here is an Omarchy web app pointed at that gateway, with a launcher that gets the gateway there first.

What's in it

  • omarchy-openclaw-onboard: OpenClaw's setup wizard the way this flow needs it. Bare openclaw onboard is the guided flow as of 2026.9.1, which ends in a foreground gateway and a browser tab and never returns; the helper runs the classic quickstart wizard with the service install under script(1) and ends it once the gateway answers and its output has gone quiet, since the wizard has no exit of its own without the TUI step.
  • omarchy-launch-openclaw: runs that onboarding in a floating terminal if it never happened, installs or starts the gateway unit when it is missing or stopped, then opens the Control UI through its single-use browser handoff URL. --tui [--message] attaches OpenClaw's terminal UI to the same gateway instead, which is what the default-agent launch uses (openclaw chat would refuse to start while the gateway owns the state dir).
  • omarchy-install-ai-openclaw / omarchy-remove-ai-openclaw: package, web app entry, and icon; removal tears down every user unit OpenClaw installed (gateway, and the node host if there is one) via upstream's own uninstall (falling back by hand), aborts if a service will not stop, and then asks, default no, whether ~/.openclaw should go too, showing its size: chats and credentials live there next to hundreds of megabytes of plugin runtimes OpenClaw downloads for itself.
  • omarchy-install-openclaw-cli plugs into the agent_installer seam from Add Hermes as a desktop app and a coding agent #7469, so omarchy default agent openclaw installs the package like Hermes installs its runtime.
  • Menu entries, a traced lobster glyph at U+E90C in the icon font (E90B is left for Perplexity, in flight on another branch), manual paragraph, and tests: 17 new cases across the menu, launcher, remover, and default-agent suites.

Verified on a real machine

Against OpenClaw 2026.9.1 from the companion omarchy-pkgs PR: menus render, launcher brings the Control UI up from unit-absent, unit-stopped, and running states, the app-grid entry does the same, omarchy agent opens the TUI attached to the gateway, and Remove > AI leaves no unit, entry, or icon behind. Full suite: 3082 ok; the two failures (runtime-smoke-test, ssh-reconnect-test) reproduce on pristine quattro / pass standalone and are untouched here.

One upstream note: as of 2026.9.1 openclaw dashboard --yes no longer installs or starts a gateway (and copies a pairing URL into the clipboard once it is up), which is why the launcher drives openclaw gateway install/start directly.

Depends on the package landing in omarchy-pkgs first: omacom/omarchy-pkgs#307.

@spencerbull

Copy link
Copy Markdown
Contributor Author

Reviewed by hand outside the scheduled triage run, on top of the PR's own "verified on a real machine" note.

Checked: the diff against quattro, the four new/changed test files (default-agent-test.sh, launch-openclaw-test.sh, menu-test.sh, remove-ai-test.sh) plus the full test/cli suite, all run on an isolated worker VM, never on the machine holding credentials — 300 assertions total, all green. The new font glyph at U+E90C is present in the shipped omarchy.ttf (fc-query confirms e90c in its charset), matching the updated menu-test assertion.

Findings:

  • [medium] default/hypr/input.lua:78, config/hypr/input.lua:48 — this commit sits directly on 49306774 ("Give foot its own touchpad scroll factor (Give foot its own touchpad scroll factor #9793)") and reverts it byte-for-byte: foot is folded back into (Alacritty|kitty|foot) at scroll_touchpad=1.5, and the comment explaining why foot needed 2.0 is gone. Neither the PR body nor the commit message mentions foot or touchpad scrolling; this looks like a rebase accident rather than an intended change. Merging as-is would silently undo Give foot its own touchpad scroll factor #9793 for anyone on foot.
  • [medium] bin/omarchy-launch-openclaw:41dashboard_url has no timeout around openclaw dashboard --json. If that call hangs, the retry loop and the "gateway did not come up" diagnostic are never reached — launching OpenClaw from the app grid can hang indefinitely with no way out but killing the process.
  • [medium] bin/omarchy-launch-openclaw:52 / bin/omarchy-remove-ai-openclaw:20 — both scripts infer the gateway's state from the unit file's existence or from systemctl/openclaw exit codes, and both have a real gap: a unit written but never successfully enabled looks "installed" to the launcher, so a later launch calls gateway start instead of repairing it, and background channels can stay disabled after login indefinitely; on removal, if gateway uninstall, systemctl disable --now, and systemctl is-active all fail (e.g. an unreachable session bus) while the gateway is genuinely still running, the script's own logic treats that as "confirmed inactive" and deletes the unit and drops the package anyway — the exact outcome its comment says it prevents.
  • [low, test-only] test/shell.d/remove-ai-test.sh's "prefers upstream's own gateway teardown" assertion doesn't clear the stub log between scenarios, so it can pass even when that scenario's run never calls gateway uninstall, because an earlier scenario already wrote that line. test/shell.d/default-agent-test.sh's --message assertions compare ${array[*]} (space-joined), so they wouldn't catch a regression that split a multi-word prompt into separate argv entries. Neither is a shipped-code bug; both weaken the regression coverage for the findings above.

Second opinion: codex at xhigh reasoning, read-only, independently re-derived the input.lua revert from git history (agreement, not independence-guaranteed) and contributed the other three findings above from its own control-flow analysis, which I verified against the source before including them. One codex claim I checked and rejected: it flagged the launcher's comment naming openclaw chat as a "documentation mismatch" against the tui call it actually makes — on inspection the comment explicitly names chat as the path being deliberately avoided and then uses tui, so this is consistent, not a mismatch.

Verdict: NEEDS AUTHOR. Nothing pushed — this review reports findings rather than fixing them.

Waiting on: the author to confirm whether the touchpad-scroll hunk belongs in this PR, and to decide whether the three script-logic findings are worth fixing before merge or as fast-follows.

🤖 Generated by Claude Sonnet 5 in Claude Code. Reviewed by Codex XHigh.

@spencerbull

Copy link
Copy Markdown
Contributor Author

Thanks, all four landed in 3a4fd9e.

  • Touchpad-scroll revert: rebase accident, exactly as you read it. The branch was collapsed onto origin/quattro with a tree that predated Give foot its own touchpad scroll factor #9793. Both input.lua files are restored from quattro; the diff is back to the 14 OpenClaw files.
  • Hanging probe: dashboard_url now runs under timeout 10, and the gateway install/start calls under 120s/60s, so an app-grid launch always reaches the "gateway did not come up" exit. New test: "bounds a hanging dashboard probe".
  • Half-installed unit: the launcher branches on systemctl --user is-enabled rather than the unit file. Not enabled (missing, or written but never enabled) gets gateway install --force, which rewrites and enables it; enabled-but-stopped gets gateway start. Verified on a real 2026.9.1 install by disabling the unit and launching: it came back enabled and active. The remover now also clears the .bak that --force leaves next to the unit.
  • Unreachable session bus on removal: only systemd's own inactive/failed answer counts as stopped; a non-zero exit with no state aborts the removal. New test: "aborts when systemd cannot be reached".
  • Tests: the "prefers upstream teardown" scenario clears the stub log first and asserts the manual disable never ran; the two --message assertions compare argv element-wise (five entries, prompt as one) or via per-entry stub logging, so a split prompt now fails.

Focused suites: launcher 8, remove 18, default-agent 34, menu 121, hermes-cli 34, all green.

@spencerbull
spencerbull marked this pull request as draft September 5, 2026 01:53
@spencerbull
spencerbull force-pushed the add-openclaw-ai-app branch 2 times, most recently from 6198cfe to 1f0699c Compare September 5, 2026 02:39
@spencerbull

Copy link
Copy Markdown
Contributor Author

One more change in 1f0699c, from testing removal end to end on a machine that had an older OpenClaw before this: omarchy-remove-ai-openclaw now tears down the node-host unit as well as the gateway (a legacy openclaw-node.service pointing at a deleted path was left enabled and failing), resets their failed state after the unit files go, and asks, default no, whether to delete ~/.openclaw with its size shown. Upstream's own uninstall keeps that directory by default too, but a third of a gigabyte of downloaded plugin runtimes hides in there next to the chats, so the leftover is now a visible choice rather than a silent one. Without a terminal nothing is asked and the state stays; the test suite covers that path. Verified live: after the run, no package, units, wants-symlinks, processes, app entry, or icon remained.

@spencerbull

Copy link
Copy Markdown
Contributor Author

b27389a fixes the first-install flow, found by running Install > AI on a machine with no OpenClaw config. Bare openclaw onboard is upstream's guided flow as of 2026.9.1: after inference passes it starts a foreground gateway and hands off to a browser tab, and never returns to the terminal. So the install script sat waiting, no gateway service was ever installed, and the Control UI opened as a plain Chromium tab rather than the app. All three onboarding call sites now run openclaw onboard --flow quickstart --install-daemon --skip-ui: --install-daemon keeps it to the classic wizard (which is what the docs call quickstart's minimal-prompt mode) and installs the gateway as the user service everything else expects, and --skip-ui drops its closing Control UI/TUI prompt since Omarchy opens the app next. Verified the classic wizard stays in the terminal and opens no listener or browser from a scratch home; launcher and default-agent tests updated to pin the flags.

@spencerbull

Copy link
Copy Markdown
Contributor Author

Follow-up on the PR head: the classic wizard fixed the browser handoff but exposed a second upstream problem. With --skip-ui the wizard prints "Onboarding complete" and then never exits: setup-*.js only calls runtime.exit(0) when the TUI was launched, and the model sign-in leaves a keep-alive TLS socket that keeps Node alive. So the install script still never reached the app launch. Onboarding now goes through a new omarchy-openclaw-onboard, which runs the wizard under script(1) (a real pty for its prompts, output mirrored to a file), and once the gateway answers and output has been quiet for 8s, ends the wizard and returns success. That point is safe because in quickstart mode every prompt precedes the service install; only closing notes follow. Its own test covers the lingering, abandoned, and clean-exit cases.

@spencerbull

Copy link
Copy Markdown
Contributor Author

Correction to the previous comment: the watchdog worked but wrapping the wizard in script(1) mangled its rendering (its @clack/prompts line output collapsed onto one line in the real terminal). Dropped script entirely. The wizard now runs directly in the foreground, so its prompts render and take input exactly as upstream draws them, while a background watcher polls the gateway and stops the wizard once the dashboard answers. Same guarantee as before (every quickstart prompt precedes the service install), without a pty wrapper in the path. Verified in an interactive pty harness that input reaches the wizard and it is stopped cleanly; the onboarding test covers the lingering, abandoned, clean-exit, and input-passthrough cases.

@spencerbull

Copy link
Copy Markdown
Contributor Author

Re-reviewed at 285de9f (all four previous fixes verified against source, not just taken on the comment's word).

Previous findings — all fixed, confirmed against source:

  • input.lua: both files are byte-identical to origin/quattro again; foot's separate 2.0 scroll factor is back.
  • dashboard_url is now wrapped in timeout 10, with 60s/120s bounds around gateway start/gateway install --force.
  • Gateway state is now read from systemctl --user is-enabled/is-active's actual reported state, not raw exit codes or file existence — a half-installed unit gets repaired instead of misclassified, and removal aborts rather than fails-open when systemd can't be reached.
  • The two weak test assertions are now precise (log cleared between scenarios; the --message argv checked element-wise).

Targeted suite (186 assertions across the five shell.d files touched, including the new openclaw-onboard-test.sh) plus the full test/cli (116) ran clean on a fresh isolated worker VM, never on this machine.

New this round — the new bin/omarchy-openclaw-onboard wrapper, introduced to fix the previous findings, has its own defects:

  • [medium] omarchy-openclaw-onboard:38 — the wizard is only ever killed once gateway_answers succeeds; there's no deadline on the wrapper itself, only on the probes inside it. If config gets written (entering the documented never-exiting state) but the gateway service then never comes up at all — port conflict, missing dependency, whatever — the wizard hangs forever, and so does everything that calls this wrapper (omarchy-launch-openclaw, omarchy-install-ai-openclaw). This is the exact failure mode the last three rounds of fixes were chasing, one level up.
  • [medium] omarchy-openclaw-onboard:29gateway_answers only checks that config exists and the dashboard responds, not that this run's wizard produced them. omarchy-launch-openclaw:75 tells a user to rerun omarchy-openclaw-onboard when onboarding didn't finish; if they do that while an old, already-configured gateway is still running, the watcher sees success immediately and sends TERM to the freshly-spawned wizard after settle_seconds (3s default) — possibly mid-prompt.
  • [low] omarchy-openclaw-onboard:26,57mktemp -u plus a non-atomic echo "$BASHPID" >"$pid_file" leaves a narrow window where the watcher's -f check can see an empty file; if hit, target is empty, kill -0 "" fails once, and the watcher quietly gives up rather than retrying — the never-exiting wizard then runs unwatched. Bash's execution model makes this unlikely to fire in practice, but it's a real TOCTOU, not a hypothetical.
  • [low] the only trap is on EXIT (which just removes the pid file) — a TERM/INT/HUP aimed at the wrapper's own PID rather than its process group can skip the explicit kill "$watcher"; wait cleanup and orphan the background watcher.
  • [low] the new command has a summary and isn't marked hidden, but GROUP_DESCRIPTIONS in bin/omarchy has no openclaw entry — it routes fine but is invisible from top-level omarchy help, which agents/skills/command-metadata.md says to keep in sync for anything users are meant to browse to.

Second opinion: codex at xhigh reasoning, read-only, ran ShellCheck over all four production scripts (clean) and independently confirmed each of the four original fixes against source — agreement, not independence-guaranteed, on those four. It found the two medium hang/mis-signal defects above from its own control-flow analysis, verified against source before including. The pid-file race I'd flagged as a question going in; codex assessed it as real but narrow, which is the same conclusion I'd reached independently.

Verdict: NEEDS AUTHOR. Nothing pushed — this is a report, not a fix.

Waiting on: the author, on whether the hang case and the stale-gateway mis-signal in the onboarding wrapper are worth another round before merge, or a fast-follow given how much ground this round already covered.

🤖 Generated by Claude Sonnet 5 in Claude Code. Reviewed by Codex XHigh.

@spencerbull
spencerbull force-pushed the add-openclaw-ai-app branch 2 times, most recently from 8abddaf to 9b7bcdb Compare September 5, 2026 04:54
@spencerbull

Copy link
Copy Markdown
Contributor Author

All five taken in 9b7bcdb. The wrapper is restructured rather than patched, since three of the findings shared a root: the pid file and the separate watcher process.

  • No deadline (medium): the gateway wait is now bounded. It counts from the moment the config exists, i.e. once the wizard has applied setup, so prompts take as long as the user takes. If the gateway has not answered 180s after that, the wizard is stopped and the wrapper exits 1 with a pointer to openclaw gateway status. Test: "a gateway that never comes up ends the wait with a failure".
  • Stale-gateway mis-signal (medium): the wrapper checks first. If the config exists and the gateway already answers, it says so, points at openclaw onboard --classic, and exits 0 without starting the wizard. So the success signal can only ever come from a gateway that was down when this run began. Test: "an already-running OpenClaw is left alone".
  • pid-file race (low): gone. The wizard is now a background job with the terminal kept as its stdin (<&0 &), so its pid is $! directly and the wrapper polls in its own loop; no pid file, no watcher process. Job control is off in a script, so the job stays in the terminal's foreground process group, reads from it freely, and receives Ctrl-C directly. Verified in a pty harness that input reaches it and that prompts render exactly as upstream draws them.
  • Signal orphaning (low): with no watcher process there is nothing to orphan, and INT/TERM/HUP at the wrapper's own pid now forward TERM to the wizard. Test: "stopping the wrapper stops the wizard".
  • Help group (low): GROUP_DESCRIPTIONS[openclaw] added; omarchy help lists the command.

Onboarding suite is now 8 cases; launcher 8, remove 18, default-agent 34, menu 121, hermes-cli 34, all green.

@spencerbull

Copy link
Copy Markdown
Contributor Author

Re-reviewed at 9b7bcdb (the onboarding wrapper rewrite). 4 of 5 findings from last round are solid; 1 is only partially closed.

Solid, confirmed against source:

  • Stale-gateway mis-signal: the up-front [[ -f $config ]] && gateway_answers check now exits before the wizard ever starts, so an already-configured, already-running gateway can't get a fresh wizard killed mid-prompt.
  • PID-file race: gone at the root — there's no more separate watcher process or pid file to race. The wrapper backgrounds the wizard directly and polls its own $!.
  • Signal orphaning: trap 'stop_wizard' INT TERM HUP reaches the wizard directly for TERM/HUP. For Ctrl-C specifically, codex ran an actual bash 5.3 PTY probe (not just static reading) and found the wrapper's comment overstates the mechanism — bash normally leaves an async child's SIGINT ignored without job control, so Ctrl-C doesn't reach the wizard directly; it works because the wrapper's own INT trap catches it and forwards TERM. Functionally fine, but worth a comment fix so the next reader isn't misled about why this is safe.
  • Help listing: confirmed live on a worker — omarchy (bare) now lists openclaw OpenClaw agent platform setup, and omarchy commands --all routes all six openclaw-related commands correctly.

Partial — [medium] bin/omarchy-openclaw-onboard:59-63: the 180s gateway timeout is armed by "the config file exists," not "this run wrote it." The up-front check only skips the wizard when the gateway is already answering; if a stale config exists from a previous, incomplete setup and the gateway is down, the wizard starts fresh, and config_seen_at gets set on the very first loop tick (~2s in) because the old file is already there — starting the 180s clock before the current run has done anything. A user re-running onboarding to repair a broken setup (exactly what omarchy-launch-openclaw:74's own error text suggests doing) who takes longer than the remaining budget to get through the prompts gets killed mid-prompt and told "gateway did not come up," which isn't what actually happened. Not tested: the only pre-existing-config case has an already-answering gateway; the timeout case starts with no config at all.

Second opinion: codex at xhigh reasoning, read-only, independently derived the same stale-config timing gap from source (I'd flagged it as a hypothesis going in; codex reached it independently too and pinned the exact lines) and additionally ran a real PTY probe to check the Ctrl-C claim empirically rather than just reading the code — that's new verification, not just agreement.

Test suite (189 assertions across the five shell.d files, including the now-8-case onboarding suite) plus the full test/cli (116) ran clean on a fresh isolated worker VM.

Verdict: NEEDS AUTHOR, for the one partial item. Nothing pushed.

Waiting on: the author, on whether config_seen_at should instead be tied to this run actually producing/updating the config (e.g. capture the file's state before starting the wizard, or record a timestamp at wizard launch and only start the clock once the file's mtime is at or after it) rather than merely its existence.

🤖 Generated by Claude Sonnet 5 in Claude Code. Reviewed by Codex XHigh.

@Chessing234 Chessing234 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

install path is careful about onboarding vs launch, and the control ui stays on 127.0.0.1. main risk is packaging/deps landing before these helpers — worth a smoke install from a clean machine before merge.

@spencerbull

Copy link
Copy Markdown
Contributor Author

Both taken in ed5a41d.

  • Deadline armed by a stale config (medium): the wrapper now records a timestamp at launch and only starts the 180s clock once the config exists and is not older than that mark, so a config left by an earlier, incomplete setup no longer counts. A user re-running onboarding to repair a broken setup can take as long as they need at the prompts. Two new tests: a stale config with a slow wizard is left to finish on its own, and the same config rewritten by this run does arm the deadline.
  • Ctrl-C comment (low): rewritten to say what codex measured: bash starts async children with SIGINT ignored when job control is off, so Ctrl-C reaches the wizard through the wrapper's INT trap forwarding TERM, not directly.

One limit worth stating rather than hiding: a repair run whose wizard neither rewrites the config nor brings the gateway up, and then hangs, is left to the user's Ctrl-C. There is no signal that separates "still at a prompt" from "hung" in that case, and guessing would reintroduce the mid-prompt kill this round removed.

Onboarding suite is 10 cases; launcher 8, remove 18, default-agent 34, menu 121, hermes-cli 34, all green.

@spencerbull

Copy link
Copy Markdown
Contributor Author

@Chessing234 agreed on the ordering risk. This PR's Install > AI entry runs omarchy-pkg-add openclaw, so it only works once omacom/omarchy-pkgs#307 has merged and the package has published to the repo; until then the entry fails on any machine that has not had the package installed by hand. Merge order is #307 first, then this.

On the smoke install: the closest run so far was on my own machine, taken through Remove > AI to a state with no package, no units, no config, no desktop entry, and then Install > AI again from the menu, which is where the two onboarding problems fixed above were found. It is not a clean image though. Once #307 has published I will run the flow on a fresh Omarchy VM from the menu and report here before asking for merge.

@spencerbull

Copy link
Copy Markdown
Contributor Author

Re-reviewed at ed5a41d (the stale-config deadline fix). Fixed what it targeted, but the refactor around it opened a related gap.

Confirmed fixed: config_applied()'s [[ -f $config && ! $started -nt $config ]] correctly distinguishes a stale pre-existing config from one this run wrote — a tie in mtimes counts as applied (correct), an older stale file does not arm the deadline (correct). The "stale config, slow wizard, left to finish" test would have failed against the previous round's code and passes now; that's a real regression test. The "rewritten config arms the deadline" test is a valid positive-case guard but isn't itself a before/after regression test — the previous round's code would also have passed it, since it only pins the arming side, not the fix.

New regression this round — [medium] bin/omarchy-openclaw-onboard:76: the polling loop now checks gateway_answers unconditionally, before config_applied is checked (that gate was moved down to guard only the deadline-arming line, at line 83). If some gateway process unrelated to this run is reachable at the default probe target while this run's config genuinely doesn't exist yet, the first gateway_answers check can return true within a couple seconds and the wrapper treats the freshly-launched wizard as having succeeded, killing it mid-prompt. This isn't hypothetical: I checked the installed OpenClaw 2026.9.1 package on this machine (dashboard-BL6a1Dus.js) — resolveDashboardTarget() only throws when the config file exists and is invalid; a config file that's simply absent falls back to {} and the dashboard command still probes the default loopback target, so dashboard --json can report ok:true with zero config on disk if anything is listening there. This is the same class of mis-signal as the round-2 finding, reintroduced through the polling loop instead of the preflight check. Fix is the one already suggested: gate the success check with config_applied || the same as the deadline check, not just the deadline.

One codex claim checked and rejected: it flagged the EXIT trap (trap 'rm -f "$started"' EXIT) as broken due to quote-escaping, leaking the marker file on every run. Read directly, the trap has no escaping at all — it's a single-quoted action containing a plain double-quoted variable reference, which is correct, ordinary bash. I found no leaked file in the worker runs either. Rejected.

Confirmed accurate: the Ctrl-C comment rewrite matches what the previous round's PTY probe found — bash ignores SIGINT for an async child without job control, so Ctrl-C works via the wrapper's own INT trap forwarding TERM, not directly.

Test suite (191 assertions across the five shell.d files, now 10 onboarding cases) plus the full test/cli (116) ran clean on a fresh isolated worker VM — expected, since neither new test covers the "gateway reachable, no config" combination the regression above lives in.

Verdict: NEEDS AUTHOR. Nothing pushed.

Waiting on: the author, to apply config_applied || in front of the success check the same way it already guards the deadline, and ideally a test that starts with no config and a gateway already reachable at the probed target (simulating an orphaned prior gateway process) to pin it.

🤖 Generated by Claude Sonnet 5 in Claude Code. Reviewed by Codex XHigh.

@spencerbull

Copy link
Copy Markdown
Contributor Author

Valid, and taken in 9ad186e. Your reading of resolveDashboardTarget() matches what I found: with no config on disk dashboard --json still probes the default loopback port, and an orphaned gateway is not hypothetical here either, since the guided flow's foreground gateway from an earlier run was exactly that.

The success check is now gateway_ready, which requires all three of: this run applied setup (config_applied, the same gate as the deadline), the unit the wizard installs is active (systemctl --user is-active openclaw-gateway.service), and the dashboard answers. The unit check is there because gating on the config alone only moves the problem: after setup, an orphan holding the port would still answer while the new service failed to bind, and the app would open on the orphan.

Two new tests pin it: a gateway answering with no config leaves a slow wizard to finish on its own, and after setup an orphan answering while the unit never becomes active ends the wait as a failure. I checked the first is a real regression test by running the suite against the previous helper: it fails there and passes now.

Onboarding suite is 12 cases; launcher 8, remove 18, default-agent 34, menu 121, hermes-cli 34, all green.

@spencerbull

Copy link
Copy Markdown
Contributor Author

Re-reviewed at 9ad186e (the gateway_ready() fix). Closes half the gap the last round found; the other half survives.

Confirmed fixed — the pre-config half: gateway_ready()'s config_applied || return 1 correctly stops an orphan gateway (one that predates this run, answering with no config on disk) from ending the wizard early. Both new tests are real regression tests — I checked they'd fail against the previous round's code (ed5a41db) and pass against this one, matching what the author already reported having done themselves.

Not fully closed — [medium] bin/omarchy-openclaw-onboard:80: systemctl --user is-active --quiet openclaw-gateway.service doesn't prove the responding gateway is the unit — only that a process is running under it. I checked the actual installed OpenClaw 2026.9.1 package's generated unit (/usr/lib/node_modules/openclaw/dist/systemd-unit-gpoQy00I.js:49-50): it sets ExecStart= and Restart=always but no Type=, so systemd defaults to Type=simple, which marks the unit active the moment the process is forked — not once it's actually bound its port. So the sequence the last round's fix aimed to close can still happen after config is written: an orphan already holds the port and keeps answering; onboarding installs/restarts the real unit; systemd reports it active immediately, before it discovers the port is taken; a poll landing in that window sees all three of gateway_ready's checks pass (config applied, unit active, dashboard answers) and ends the wizard — with the app then pointed at the orphan, not the gateway this run actually installed. The unit would go on to fail or restart-loop (5s delay) separately, unobserved.

The two new tests don't catch this because the systemctl stub equates "unit file exists" with "active," which only models installed-vs-absent, not the active-but-not-yet-bound transient the real unit can produce.

Second opinion: codex at xhigh reasoning, read-only, found this by reading the actual installed OpenClaw package's systemd unit generator rather than reasoning about is-active in the abstract — I verified the Type=/Restart=always claim against that same file myself before including it. It also confirmed the two new tests are genuine regression tests (not vacuous), and confirmed the launch/remove scripts and the rest of the onboard script are untouched this round.

Test suite (193 assertions, now 12 onboarding cases) plus the full test/cli (116) ran clean on a fresh isolated worker VM — expected, since this gap is in the systemd-timing dimension neither test models.

Verdict: NEEDS AUTHOR. Nothing pushed.

Waiting on: the author, on how to make gateway_ready prove the responding gateway is actually owned by the unit rather than just co-active with it — e.g. resolving the process actually bound to the gateway's port and comparing it against the unit's MainPID, rather than trusting is-active alone.

🤖 Generated by Claude Sonnet 5 in Claude Code. Reviewed by Codex XHigh.

OpenClaw's desktop experience on Linux is its Control UI, served by the
gateway the openclaw package runs, so the Install > AI entry installs
the package and a web app launcher that routes through the new
omarchy-launch-openclaw: first launch hands off to OpenClaw's own
onboarding wizard, later launches start the gateway when needed and open
the dashboard's single-use browser handoff URL as an app window.
Remove > AI tears the gateway service down through OpenClaw's own
gateway uninstall (falling back to systemctl by hand), aborts rather
than dropping the package under a gateway that will not stop, and keeps
the user's agent in ~/.openclaw.

OpenClaw also joins Setup > Defaults > Agent through the same
agent_installer seam Hermes carries: its CLI is the pacman package
rather than a mise tool, so omarchy-install-openclaw-cli answers
--check/--now with pacman, and omarchy-agent runs `openclaw chat`,
seeding prompts through --message.

The menu mark is a new U+E90C glyph traced from the package's lobster
favicon; E90B stays free for the Perplexity mark still in flight on its
own branch.
The launcher recovers the gateway through `openclaw gateway install --force`
(unit not enabled: missing, or an install that died after writing it) or
`openclaw gateway start` (enabled but stopped), never `openclaw dashboard
--yes`: as of OpenClaw 2026.9.1 that defers to "the owning supervisor" in
both cases, and once the gateway is up it copies a one-time browser pairing
URL into the clipboard. The dashboard probe is bounded so an app-grid launch
cannot hang without a terminal to interrupt it. Removal treats only
systemd's own "inactive"/"failed" as a stopped gateway, so an unreachable
user manager aborts instead of dropping the package under a live process.
All of it verified against a real 2026.9.1 install.
Removal also takes down the node-host unit if OpenClaw ever installed one, and
asks (default no, only on a terminal) whether ~/.openclaw should go too, with
its size: the chats and credentials live there next to hundreds of megabytes
of plugin runtimes and cache OpenClaw downloads for itself.
Onboarding goes through omarchy-openclaw-onboard rather than bare `openclaw
onboard`: as of 2026.9.1 the bare command is the guided flow, which ends by
running a foreground gateway and handing off to a browser tab without
returning, so the install script never reached the app launch and no service
was installed. The helper runs the classic wizard (--flow quickstart
--install-daemon --skip-ui) as a background job that keeps the terminal as its
stdin, so its prompts render and take input as upstream draws them, and stops
it once the gateway answers: upstream leaves the wizard running after its
outro (only the TUI branch exits, and the model sign-in holds a socket open).
Every quickstart prompt precedes the service install, so that point is safe.
A gateway that never comes up after this run applies setup ends the wait as a
failure instead
of hanging, an already-running OpenClaw is left alone rather than mistaken for
this run's success, a gateway answering on the port is only this run's once its
process is the unit's own MainPID (an orphan from an
earlier run) is not mistaken for the service this run installs, and a signal at
the helper takes the wizard down with it.
@spencerbull

Copy link
Copy Markdown
Contributor Author

Valid, and taken in 5345673. I confirmed the unit generator sets no Type=, so is-active proves nothing about the port.

gateway_ready now proves ownership instead: it reads the port from dashboard --json, resolves the process bound to it with ss -ltnp, and requires that pid to equal the unit's MainPID (systemctl --user show -p MainPID --value). The is-active check is gone; a non-zero MainPID that matches the listener implies both. Checked against the real gateway on this machine: the listener pid is the unit's MainPID and its cgroup is the unit, and the function returns true there.

The stubs now model this dimension: systemctl answers MainPID with 4242 once the unit exists, and ss reports 4242 as the listener unless a test plants an orphan pid. The orphan-after-setup test is reshaped into exactly the sequence you described: the wizard writes config and unit, the unit is "active" with MainPID 4242, but pid 999 holds the port. The wrapper no longer accepts that; the deadline ends the wait as a failure. Ran the suite against the 9ad186e helper to confirm it is a real regression test: that case fails there and passes now.

Onboarding suite is 12 cases; launcher 8, remove 18, default-agent 34, menu 121, hermes-cli 34, all green.

@spencerbull

Copy link
Copy Markdown
Contributor Author

Re-reviewed at 5345673 (the PID-ownership fix). This one closes it — no defects found this round.

Confirmed solid: gateway_ready() now requires the port's actual listener PID (via ss -ltnpH "sport = :$port") to equal the unit's MainPID (via systemctl --user show -p MainPID --value), with both non-empty and non-zero. This closes the Type=simple-forked-but-not-yet-bound race from last round: an active-but-not-yet-listening unit no longer gets confused with an orphan still holding the port. I checked the parsing against this machine's real ss -ltnp output format (users:(("name",pid=NNNN,fd=N))) and the sed extraction handles it correctly. Codex went further and found that OpenClaw's own installed source (ports-inspect-B8sIktfP.js:344) uses the identical ss ... "sport = :${port}" filter to answer the same question — the wrapper's technique mirrors upstream's own approach to port ownership, not an invented one.

The reshaped test (unit active with MainPID=4242, but a planted listener pid 999 holding the port) is a genuine regression test: it fails against last round's code (which would see unit-active + dashboard-success and return success) and passes against this one.

Second opinion: codex at xhigh reasoning, read-only, verdict SHIP/GO — no concrete defects. It independently confirmed .port appears in the real dashboard --json response, confirmed the generated unit's MainPID really is the gateway process (not some wrapper), and reasoned through the remaining theoretical PID-reuse window as not actionable given this host's pid_max.

Test suite (193 assertions, 12 onboarding cases) plus the full test/cli (116) ran clean on a fresh isolated worker VM.

Verdict: SHIP (onboarding wrapper). Nothing pushed — this was a report, as every round has been.

Six rounds against this PR now: the touchpad-scroll revert, the hang/timeout gaps, the stale-config and two orphan-gateway races are all fixed and independently verified, each against the actual installed OpenClaw package or a real worker VM rather than assumption. What's still open isn't code in this repo: @Chessing234's packaging-order point (this depends on omacom/omarchy-pkgs#307 landing and publishing first) and the fresh-VM smoke test the author already said they'd run and report here before asking for merge.

🤖 Generated by Claude Sonnet 5 in Claude Code. Reviewed by Codex XHigh.

@spencerbull
spencerbull marked this pull request as ready for review September 5, 2026 14:42
@spencerbull
spencerbull merged commit eb56446 into quattro Sep 5, 2026
@spencerbull
spencerbull deleted the add-openclaw-ai-app branch September 5, 2026 14:44
ryanrhughes pushed a commit that referenced this pull request Sep 8, 2026
Add OpenClaw as a desktop app and a coding agent

(cherry picked from commit eb56446)
(cherry picked from commit bde2584b5596412142d0a2039d44dd6a25949517)
ryanrhughes pushed a commit that referenced this pull request Sep 8, 2026
Add OpenClaw as a desktop app and a coding agent

(cherry picked from commit eb56446)
(cherry picked from commit bde2584b5596412142d0a2039d44dd6a25949517)
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.

2 participants