Skip to content

Close Mac/Linux gaps: daemon-failure alert, K2 on Sway and X11, host tests - #153

Open
algorithmx wants to merge 7 commits into
second-state:mainfrom
algorithmx:linux-parity
Open

algorithmx wants to merge 7 commits into
second-state:mainfrom
algorithmx:linux-parity

Conversation

@algorithmx

Copy link
Copy Markdown

Closes part of the gap between the Mac app (app/) and the Linux app (desktop/). docs/platform-parity.md (new) compares the two row by row, names the code each row rests on, and ranks what's still missing. Five commits, each reviewable on its own:

  1. Test the Linux daemon against a simulated box. tools/test-linux-host.sh runs the real vibebuddyd against the firmware core's simulator through a PTY: no box, desktop, user config or network. The simulator gains a test-only control socket (buttons, clock, reboot). Desktop helpers (hyprctl, swaymsg, wmctrl, notify-send, systemctl, …) are stubbed and recorded. CI runs it on Ubuntu.
  2. Tell the Linux user when the daemon fails, as the Mac does. The Mac notifies when its supervisor gives up; Linux said nothing. Now the app sends one notify-send after 30 s without the daemon, again only after a recovery. A unit stopped on purpose stays silent but is rechecked, so a failed restart after it still notifies.
    • Restart daemon on Advanced works while the daemon is down (through systemd, like the tray).
    • The Linux status stream has header (10 s) and inactivity (45 s) deadlines, so a stalled daemon counts as down.
    • Mac: the stream uses the same 45 s inactivity timeout, and the failure notification follows the last reported "Notify me" setting instead of falling back to on once status is cleared.
  3. Focus K2 windows on Sway and X11, not only Hyprland (daemon/src/window_focus.rs: swaymsg by container id, wmctrl by window id). The window-system variables are read from systemctl --user show-environment at each press, so a daemon systemd started before the desktop imported them still finds the session. The AUR package lists wmctrl as optional. GNOME/KDE under Wayland still get only the tmux pane switch.
  4. Show the daemon log in the desktop's own terminal. It falls back from xdg-terminal-exec (missing on Ubuntu 22.04) to GNOME Terminal, Konsole, Xfce Terminal and xterm. With none installed, Advanced says which command to run. The Mac has no such button; its daemon logs to a file.
  5. Docs: platform-parity.md, linux-host-testing.md, README (both languages), app.md, architecture.md, two LESSONS entries.

UI strings have zh-Hans translations (286 keys).

Validation

On Linux (Ubuntu), from 574b700:

  • cargo test --workspace --locked: 525 passed, 1 ignored (the native-USB ROM test).
  • cargo clippy --workspace --all-targets --locked: only the four warnings main already has.
  • tools/check-localization.py and the Python tool tests pass.
  • tools/test-linux-host.sh: 55 scenarios pass (28 device/host, 11 daemon outage, 9 Sway, 7 X11).

Not run: the Swift changes are not compiled here (the mac-app job will be their first build); no real Sway or X11 login; no real notification server or systemd unit; no tray on Fedora/upstream GNOME/KDE; no box attached. Those stay listed as pending in platform-parity.md.

🤖 Generated with Claude Code

algorithmx and others added 5 commits October 17, 2026 16:33
tools/test-linux-host.sh runs the real vibebuddyd against the firmware core's
simulator, joined through a PTY, with no box, desktop, user config or network.
The simulator gains a test-only control socket (button levels, clock advance,
reboot) and a --build override; desktop command helpers are stubbed and logged.
A headless probe (host_status_probe) feeds live snapshots through the Linux
app's status and tray model.

The 28 scenarios cover boot identity and pairing, activity states, K2 routing,
local K0/K1, unplug and replug, Wi-Fi signals, voice and Character pack writes
and their failure paths, the status stream, malformed device output,
screenshots and foreign firmware. CI runs the suite on Ubuntu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Mac app notifies when its supervisor gives up on the daemon; the Linux
app, whose daemon systemd supervises, said nothing. It now notifies through
notify-send once after 30 s without the daemon, and again only after a
recovery. A unit stopped on purpose (inactive or deactivating) stays silent
and is rechecked after another 30 s, so a failed restart after a stop still
notifies. An unreadable unit state, or a systemctl that doesn't answer, still
notifies. The policy lives in desktop/src/outage.rs; daemon_outage_probe
drives it headless from the real status stream.

- Restart daemon on Advanced works while the daemon is down: Advanced and the
  tray both restart through systemd and report a failure, as the Mac's button
  calls its supervisor.
- The Linux status stream bounds response headers to 10 s and inactivity to
  45 s, so a daemon that stalls with the socket open counts as down; SSE
  keepalives keep a quiet healthy stream alive.
- Mac: the stream uses the same 45 s inactivity timeout, and the failure
  notification follows the last reported "Notify me" setting instead of
  defaulting to on once the status is cleared.

Eleven host scenarios cover the grace period, recovery, the toggle, an
intentional stop, a failed restart after it, crash loops and slow or failing
systemctl. Not run on a real desktop; the Swift changes are not compiled here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On the Mac, K2 brings the agent's terminal or app forward on any desktop. On
Linux it focused a window only on Hyprland. daemon/src/window_focus.rs adds
two backends that list windows with their pids and focus the one owned by the
nearest of the agent's ancestors: Sway (`swaymsg -t get_tree`, then focus by
container id) and X11 (`wmctrl -l -p`, then raise by window id). Hyprland
stays the default; Sway is chosen only for a SWAYSOCK that exists, X11 only
for an X11 session, so XWayland under a Wayland compositor is not X11.

The window-system variables come from `systemctl --user show-environment` at
each press, then from the daemon's own environment: systemd can start the
daemon before the desktop imports them, and the user manager has what was
imported since. The helper runs with those variables.

The AUR package lists wmctrl as optional. GNOME and KDE under Wayland still
get no window focus; a tmux pane switch still happens.

Sixteen host scenarios run each backend against recorded listings: nearest
ancestor, needs-input priority, a source surviving a restart, missing windows,
malformed output, refused, failing and slow helpers, a stale Sway socket and
XWayland. Not run on a real Sway or X11 login.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Show daemon log ran only xdg-terminal-exec, which Ubuntu 22.04, for one,
doesn't have, so the button did nothing there. It now falls back to GNOME
Terminal, Konsole, Xfce Terminal and xterm. Only a terminal that isn't
installed moves on to the next; one that fails to start is reported. With
none installed, Advanced says which command shows the log (zh-Hans included).
The Mac has no such button: its daemon logs to a file in the logs folder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/platform-parity.md compares the two apps row by row (settings,
notifications, tray, K2, paths and packaging), names the code each row rests
on, ranks the gaps that remain and says what has and hasn't been run. The
README (both languages), docs/app.md and docs/architecture.md describe the
daemon-failure notification, K2 on Sway and X11, and the tray on GNOME.
docs/linux-host-testing.md explains the host suite. Two lessons from writing
its scenarios go in LESSONS.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

longzhi commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator

Review notes on 6cfd1a3. Local checks: cargo test -p vibebuddyd window_focus (22 passed) and tools/test-linux-host.sh (55 scenarios passed).

1. "Restart daemon" now needs systemd — desktop/src/client.rs:162

  • Old behaviour: while the daemon ran, the app sent POST /v1/daemon/restart.
  • New behaviour: on Linux, the app always runs systemctl --user restart vibebuddyd.
  • Good result: the button now works when the daemon is down.
  • Bad result: if systemd did not start the daemon, the button fails. Examples: cargo run during development, or a system without systemd.
  • Suggestion: when systemctl fails, fall back to POST /v1/daemon/restart. Or state in the docs that this button needs the systemd unit.

2. Hyprland detection does not check that the session is live — daemon/src/window_focus.rs:52

  • compositor() accepts SWAYSOCK only when the socket exists.
  • HYPRLAND_INSTANCE_SIGNATURE wins when it is set. Nothing checks that it is still valid.
  • session() takes values from systemctl --user show-environment first.
  • Failure case:
    1. An earlier Hyprland session imported HYPRLAND_INSTANCE_SIGNATURE into the user manager (for example, with linger on).
    2. The user logs in to Sway or X11.
    3. The daemon still selects Hyprland.
    4. Each K2 press fails in hyprctl.
  • Suggestion: accept the signature only when $XDG_RUNTIME_DIR/hypr/<signature> exists, the same way the code handles Sway.

3. Comment refers to a missing document — daemon/src/source_opener.rs:526

  • The comment says "(G4 review 3.4)".
  • The repository has no document with that name.
  • Suggestion: remove the reference, or write the reason in the comment.

Generated by Claude Code

algorithmx and others added 2 commits October 24, 2026 14:39
Review of second-state#153 found two Linux features deciding from claims about the
environment instead of from the thing they act on.

K2 took the window system from systemd's user manager, which belongs to
the user rather than a session and keeps what an ended session imported,
and fell back to Hyprland when it knew nothing. A Hyprland signature left
there made every press run hyprctl on a Sway or X11 login. Now:
- the window system comes from the environment of the processes being
  focused (/proc/<pid>/environ), taken whole from the nearest graphical
  one; the user manager, then the daemon's own environment, are only a
  fallback for processes that can't be read;
- Hyprland and Sway count only while their socket accepts a connection
  (Hyprland's in $XDG_RUNTIME_DIR/hypr, or /tmp/hypr before 0.40);
- with no evidence, nothing is focused and no helper runs: the session
  "runs no supported window system".

The Linux app assumed systemd ran the daemon. For one started by hand,
Restart failed, Show daemon log showed an empty journal, and the failure
alert read the unit's `inactive` as a deliberate stop and never fired.
Now the daemon reports `daemon.supervisor` (systemd or other) from its
own cgroup, not from INVOCATION_ID, which a terminal run as a service
passes down. The app keeps the last answer while the daemon is down:
- Restart and Show daemon log say why they can't act on that daemon;
- the alert goes out without asking systemd and says to start the daemon
  again by hand;
- POST /v1/daemon/restart refuses with 409 instead of exiting, since
  nothing would start the daemon again.
The Mac app is unchanged: it already knows whether it or a LaunchAgent
supervises the daemon (managesDaemon), and the daemon reports nothing
there.

Also drops a comment's reference to private review notes.

The host harness now starts a real process for each placeholder pid with
the scenario's session environment, and real listening sockets for
Hyprland and Sway. New scenarios cover stale Hyprland and X11 variables
in the user manager, the user-manager fallback, a socket nobody listens
on, and a daemon outside the service. 61 host scenarios and 532
workspace tests pass. Breaking each new rule on purpose fails the
scenarios meant to catch it. Not run on a real Sway, X11 or GNOME
session.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/parity-scenarios/ holds 70 notes, one per thing the Mac app does,
read from its code: lifecycle and daemon supervision, the menu bar,
notifications, each Settings tab, the first-run guide and errors. Each
note gives the initial state, the trigger, the app's steps with the exact
UI strings and daemon calls, and the final state. It ends with a
checklist of questions for the Linux app and a blank result to fill in
(same, differs by design, gap or not applicable). The README indexes
them and says how to use them.

The notes describe the Mac app only. Where platform-parity.md already
records a difference as by design, the note says so; everything else is
a question, not a claim about Linux.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@algorithmx

Copy link
Copy Markdown
Author

@longzhi Thanks for the review. All three points were real, and each turned out to be one case of a wider problem, so 887ec20 fixes the cause rather than the symptom.

1. Restart without systemd

I didn't add the POST /v1/daemon/restart fallback. That endpoint only exits, so without a supervisor it would stop the daemon instead of restarting it. The same hidden assumption, that systemd runs the daemon, also broke two other things. Show daemon log opened an empty journal. Worse, the failure alert read the unit's inactive as a deliberate stop, so a crash of a hand-started daemon never notified.

  • The daemon now reports daemon.supervisor (systemd or other) in its status, from its own cgroup. It doesn't use INVOCATION_ID, because a terminal that runs as a systemd service passes it down to everything started from it.
  • The app keeps the last answer while the daemon is down. For a daemon outside the service, Restart and Show daemon log say why they can't act on it, and the failure alert goes out without asking systemd.
  • POST /v1/daemon/restart refuses with 409 when nothing would start the daemon again.
  • No Mac change was needed: the Mac app already knows who supervises its daemon (managesDaemon).

2. Stale Hyprland signature

Fixed as you suggested, and more broadly:

  • K2 now reads the window system from the environment of the processes it is about to focus (/proc/<pid>/environ), taken whole from the nearest one with a graphical session.
  • Hyprland and Sway count only if their socket accepts a connection: $XDG_RUNTIME_DIR/hypr/<sig>/.socket.sock, or /tmp/hypr/<sig> before Hyprland 0.40.
  • With no evidence, K2 focuses nothing and says so in the log, instead of assuming Hyprland.
  • systemd's user manager is only a fallback, for processes whose environment can't be read.

This also covers stale X11 variables in the user manager and a Sway socket left behind by a crashed compositor.

3. "(G4 review 3.4)"

Removed.

Tests

The host suite now starts a real process for each placeholder pid, with the session environment each scenario sets up, plus real listening sockets for Hyprland and Sway. New scenarios cover stale Hyprland and X11 variables, the user-manager fallback, a socket nobody listens on, and a daemon outside the service.

  • Host suite: 61 scenarios pass, up from 55.
  • cargo test --workspace: 532 passed, 1 ignored.
  • Clippy: only the four warnings main already has.
  • Localization: passes, 3 new zh-Hans strings.
  • Breaking each new rule on purpose made the scenarios meant to catch it fail.

Nothing has been run on a real Sway, X11 or GNOME session yet.

Parity scenario notes

7acc6a9 adds docs/parity-scenarios/: 70 notes, one per thing the Mac app does, read from its code. They cover lifecycle and daemon supervision, the menu bar, notifications, every Settings tab, the first-run guide and errors.

  • Each note gives the initial state, the trigger, the app's steps (with the exact UI strings and daemon calls) and the final state.
  • Each ends with a checklist of questions for the Linux app and a blank result: same, differs by design, gap or not applicable.
  • The notes describe the Mac app only. Where platform-parity.md already records a difference as by design, the note says so; everything else is an open question, not a claim about Linux.

The next commit will be based on these notes: I'll go through them against the Linux app, fill in the results, and fix the gaps they turn up.

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