Skip to content

feat(server): run the background service on macOS via launchd - #6286

Open
t3dotgg wants to merge 3 commits into
mainfrom
launchd-boot-service
Open

feat(server): run the background service on macOS via launchd#6286
t3dotgg wants to merge 3 commits into
mainfrom
launchd-boot-service

Conversation

@t3dotgg

@t3dotgg t3dotgg commented Aug 12, 2026

Copy link
Copy Markdown
Member

Wanted the T3 Connect/serve daemon running in the background on a Mac, the same way it works on Linux. But `t3 service install` was systemd-only and failed closed on darwin.

Now macOS gets a per-user LaunchAgent (`~/Library/LaunchAgents/com.t3tools.t3code.service.plist`). The pinned runtime, stable launcher, and remote-update protocol are reused unchanged; only the service-manager layer is new. The platform split is a small data-driven `BootServiceManager` (paths, pure renderer, command steps), so install/uninstall/status stay single-flow with no platform branches. launchctl steps that fail on already/not-loaded states are tolerated; the final `kickstart -k` stays strict so a broken setup fails loudly.

Honest semantics: a LaunchAgent starts at login and stops at logout (no linger equivalent), so onboarding copy and docs say so instead of promising boot-time behavior.

Verified end to end on an M-series Mac with an isolated `T3CODE_HOME`: install pins `t3@0.0.33` from npm, the job runs, killing the launcher gets respawned by KeepAlive, the child server carries the launcher IPC context (so remote self-update capability is advertised), and uninstall removes the job and plist cleanly. 28 tests pass across the touched files.


Built by Claude Code (Fable 5) with human direction from Theo.


Note

Medium Risk
Touches OS-level service install/uninstall and process lifecycle on a new platform (launchd), including stop/start races and timeouts. Risk is moderated by reusing the existing launcher/update protocol and adding focused macOS coverage.

Overview
Adds macOS background-service support via a per-user LaunchAgent at ~/Library/LaunchAgents/com.t3tools.t3code.service.plist, so t3 service install no longer fails closed on darwin.

Refactors boot-service integration into a data-driven BootServiceManager (systemd vs launchd: paths, plist/unit renderer, and command steps). Install/uninstall/status stay single-flow with no platform branches. Launchctl steps that commonly fail when unloaded are optional; the final bootstrap remains strict. Stop steps now use a 120s timeout so they outlast the 90s stop grace on both managers.

CLI onboarding and status copy now distinguish platforms: macOS promises login-session reachability only (no linger), Linux keeps boot/logout wording. Docs cover SSH install caveats, Login Items, and Full Disk Access for the node binary.

Reviewed by Cursor Bugbot for commit ed497ee. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add macOS launchd support to the background service manager

  • Adds a launchdManager in bootService.ts alongside the existing systemdManager, selected at runtime via selectBootServiceManager based on platform and user id.
  • The launchd agent uses a generated plist with RunAtLoad, KeepAlive, ThrottleInterval=5, and ExitTimeOut=90, writing stdout/stderr to the log file.
  • CLI prompts and post-onboarding messages in service.ts and connect.ts now vary by platform, noting macOS agents start at login and stop at logout.
  • Adds HostProcessUserId context in hostProcess.ts (via process.getuid()) used by the launchd manager to construct the GUI domain target.
  • Behavioral Change: macOS service lifecycle is login-scoped (not boot-scoped); stop/disable steps use a 120s timeout to avoid mid-shutdown cancellation on both platforms.

Macroscope summarized ed497ee.

t3 service was systemd-only. Adds a per-user LaunchAgent path that reuses
the pinned runtime, launcher, and update protocol unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b8c8a77d-762e-4de8-a4ba-cee717938a38

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Aug 12, 2026

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.

🟡 Medium

launchdManager.deactivate and launchdManager.stop mark launchctl bootout as optional: true, so runSteps ignores every non-zero exit — not just the intended "not loaded" case. During uninstall, a genuine bootout failure is followed by plist removal and a true return even though the KeepAlive agent is still running. During install/update, a failed bootout means the old job stays loaded; the subsequent bootstrap failure is also swallowed (also optional), and kickstart -k restarts the still-loaded job with the old cached plist definition — yet install returns the new plan and status only compares the plist on disk, so callers are told the update is current while launchd runs the previous configuration. Consider making bootout failures fatal (or narrowing optionality to only the "not loaded" exit case) so genuine unload failures block the flow before rewriting files.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/cloud/bootService.ts around line 269:

`launchdManager.deactivate` and `launchdManager.stop` mark `launchctl bootout` as `optional: true`, so `runSteps` ignores every non-zero exit — not just the intended "not loaded" case. During uninstall, a genuine `bootout` failure is followed by plist removal and a `true` return even though the KeepAlive agent is still running. During install/update, a failed `bootout` means the old job stays loaded; the subsequent `bootstrap` failure is also swallowed (also optional), and `kickstart -k` restarts the still-loaded job with the old cached plist definition — yet `install` returns the new plan and `status` only compares the plist on disk, so callers are told the update is current while launchd runs the previous configuration. Consider making `bootout` failures fatal (or narrowing optionality to only the "not loaded" exit case) so genuine unload failures block the flow before rewriting files.

@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 11.3 KiB 11.3 KiB −6 B (−0.1%) 15.1 KiB
Codex Thread snapshot wire 5.5 KiB 5.5 KiB −1 B (−0.0%) 7.3 KiB
Codex Live turn WebSocket wire 5.9 KiB 5.9 KiB −5 B (−0.1%) 7.8 KiB
Codex Live turn WebSocket decoded 49.7 KiB 49.7 KiB 0 B (0.0%) 66.4 KiB
Codex Live turn messages 16 16 0 (0.0%) 21
Claude Total thread wire 11.3 KiB 11.3 KiB −13 B (−0.1%) 15.1 KiB
Claude Thread snapshot wire 5.5 KiB 5.5 KiB −8 B (−0.1%) 7.3 KiB
Claude Live turn WebSocket wire 5.9 KiB 5.9 KiB −5 B (−0.1%) 7.8 KiB
Claude Live turn WebSocket decoded 50.6 KiB 50.6 KiB 0 B (0.0%) 66.4 KiB
Claude Live turn messages 16 16 0 (0.0%) 21

Baseline: 5a84614 · PR result: ed497ee · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 94.6 KiB
  • Claude decoded thread snapshot: 95.4 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

Comment thread apps/server/src/cloud/bootService.ts
@macroscopeapp

macroscopeapp Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

1 blocking correctness issue found. This PR adds a new platform feature (macOS launchd support for the background service), introducing new user-facing behavior, new service management abstractions, and new platform integration. New feature PRs of this scope warrant human review. Additionally, there is an unresolved Medium-severity finding regarding error handling for launchctl bootout failures.

You can customize Macroscope's approvability policy. Learn more.

Review findings: bootstrap of a RunAtLoad/KeepAlive plist already starts
the job, so the trailing kickstart -k killed a server it just booted.
Bootstrap is now the strict last step. ExitTimeOut raised to 90 to match
systemd's default stop timeout for update handoffs. Docs corrected for
headless SSH installs, TCC prompts, and FileVault vs auto-login.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cursor cursor Bot 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.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 653a788. Configure here.

Comment thread apps/server/src/cloud/bootService.ts
Review findings: raising ExitTimeOut to 90 put the stop above the
ProcessRunner's 60s default, so a slow shutdown got its bootout cancelled
and the strict bootstrap raced a still-loaded job. Stop and deactivate
steps on both platforms now run with a 120s timeout. Also corrects the
launchd default-timeout comment (system-defined, ~5s, not 20s), pins
ExitTimeOut and stop timeouts in tests, and documents the Login Items
toggle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
turn on automatic login (System Settings → Users & Groups; unavailable while FileVault is on) and
keep the Mac from sleeping.

Two more macOS notes:

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.

🟢 Low user/background-service.md:52

The heading says "Two more macOS notes," but the section lists three bullet points (SSH installation, privacy prompts, and Login Items). Change "Two" to "Three" or remove the count.

Suggested change
Two more macOS notes:
Three more macOS notes:
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @docs/user/background-service.md around line 52:

The heading says "Two more macOS notes," but the section lists three bullet points (SSH installation, privacy prompts, and Login Items). Change "Two" to "Three" or remove the count.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant