feat(server): run the background service on macOS via launchd - #6286
feat(server): run the background service on macOS via launchd#6286t3dotgg wants to merge 3 commits into
Conversation
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>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
There was a problem hiding this comment.
🟡 Medium
t3code/apps/server/src/cloud/bootService.ts
Line 269 in adff784
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.
Thread transfer impact✅ Thread transfer remains within every enforced ceiling.
Baseline: Scenario and decoded snapshot size10 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.
Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed. |
ApprovabilityVerdict: 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>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ 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.
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: |
There was a problem hiding this comment.
🟢 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.
| 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.

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, sot3 service installno 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 finalbootstrapremains 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
launchdManagerin bootService.ts alongside the existingsystemdManager, selected at runtime viaselectBootServiceManagerbased on platform and user id.RunAtLoad,KeepAlive,ThrottleInterval=5, andExitTimeOut=90, writing stdout/stderr to the log file.HostProcessUserIdcontext in hostProcess.ts (viaprocess.getuid()) used by the launchd manager to construct the GUI domain target.Macroscope summarized ed497ee.