Skip to content

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

Merged
t3dotgg merged 5 commits into
mainfrom
launchd-boot-service
Aug 19, 2026
Merged

feat(server): run the background service on macOS via launchd#6286
t3dotgg merged 5 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 host service installation and long-blocking launchctl/systemctl orchestration; mistakes could leave a broken or stale agent, though behavior is heavily tested and Linux paths are mostly refactored, not rewritten.

Overview
macOS can now install the same pinned-launcher background service via a per-user LaunchAgent (~/Library/LaunchAgents/com.t3tools.t3code.service.plist). Linux systemd behavior is unchanged; Windows still fails closed.

bootService is refactored around a BootServiceManager abstraction: each platform supplies paths, a pure unit/plist renderer, and declarative launchctl / systemctl step lists consumed by shared install / uninstall / status flows. macOS uses bootout --wait, optional enable/bootout steps, 120s stop timeouts (above systemd/ExitTimeOut), and plist settings aligned with the existing launcher/update model. HostProcessUserId (process.getuid() on POSIX) drives the gui/<uid> launchd domain.

Onboarding and status copy no longer promise post-logout reachability on Mac: connect and service prompts describe login-scoped availability instead of boot/linger behavior. User and internal docs now document Linux vs macOS differences (SSH install, TCC, Login Items).

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

Note

Add macOS launchd support for the background boot service

  • Adds a launchdManager in bootService.ts that installs a launch agent plist, bootstraps it at login, and handles stop/restart/uninstall via launchctl.
  • Adds renderBootServicePlist to generate a launchd plist with RunAtLoad, KeepAlive, ThrottleInterval=5, ExitTimeOut=90, and consolidated stdout/stderr logging.
  • Refactors the boot service factory to select between systemd (Linux) and launchd (macOS) via a new selectBootServiceManager abstraction, using a new HostProcessUserId context for POSIX uid.
  • Updates CLI messaging and onboarding prompts to reflect macOS login-scoped persistence vs. Linux boot-time/linger persistence.
  • Behavioral Change: on macOS the service runs only while the user is logged in (no linger equivalent); Windows remains unsupported.

Macroscope summarized d3db140.

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: 3cdab0e2-46d3-4cd5-817a-95c4e2704be1

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
Comment thread apps/server/src/cloud/bootService.ts
@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 12.6 KiB +1.3 KiB (+11.3%) 15.1 KiB
Codex Thread snapshot wire 5.5 KiB 6.3 KiB +848 B (+15.2%) 7.3 KiB
Codex Live turn WebSocket wire 5.9 KiB 6.3 KiB +468 B (+7.8%) 7.8 KiB
Codex Live turn WebSocket decoded 49.7 KiB 51.8 KiB +2.1 KiB (+4.2%) 66.4 KiB
Codex Live turn messages 16 16 0 (0.0%) 21
Claude Total thread wire 11.3 KiB 12.6 KiB +1.3 KiB (+11.2%) 15.1 KiB
Claude Thread snapshot wire 5.5 KiB 6.3 KiB +812 B (+14.5%) 7.3 KiB
Claude Live turn WebSocket wire 5.9 KiB 6.3 KiB +490 B (+8.2%) 7.8 KiB
Claude Live turn WebSocket decoded 50.6 KiB 52.7 KiB +2.1 KiB (+4.2%) 66.4 KiB
Claude Live turn messages 16 16 0 (0.0%) 21

Baseline: 5a84614 · PR result: d3db140 · 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: 99.9 KiB
  • Claude decoded thread snapshot: 100.6 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: Not approved

Macroscope's review found this PR not approvable — This PR introduces new macOS launchd support for the background service—a significant new feature with new platform integration logic. There is also an open Medium-severity finding about potential bootout compatibility issues on older macOS versions.

You can add or adjust custom eligibility rules. 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>
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>
Comment thread docs/user/background-service.md Outdated
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added the 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. label Aug 19, 2026
Verified empirically: bootout returns in milliseconds while the job
drains for up to ExitTimeOut, and a bootstrap during the drain fails
EIO — every service update over a running agent would fail. bootout
--wait (present on modern macOS, missing from the man page) blocks
until the job leaves the domain; the 120s step timeout outlives it.
Live-tested install → update-over-running → uninstall.

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 d3db140. Configure here.

{
step: "stopping the installed launch agent",
command: "launchctl",
args: ["bootout", "--wait", serviceTarget],

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.

Optional bootout skips stop without --wait

Medium Severity

launchctl bootout --wait is not available before macOS 26, but stop and uninstall still mark that step optional. An unrecognized flag fails the command without unloading the job, the failure is ignored, and install then rewrites launcher and state under a still-running agent, after which bootstrap fails. Updates and reinstalls on Sequoia-era Macs never stop the existing service.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d3db140. Configure here.

@t3dotgg
t3dotgg merged commit 2aa5f09 into main Aug 19, 2026
19 checks passed
@t3dotgg
t3dotgg deleted the launchd-boot-service branch August 19, 2026 08:04
sheehanmunim added a commit to munimtechnologies/mtcode that referenced this pull request Aug 19, 2026
…, Clerk OAuth transport (pingdotgg#7479)

Upstream's macOS launchd service lands on top of the fork's Windows support:
`launchctl bootout --wait` and its reasoning are upstream's, while the
three-platform copy and the Windows Startup-shortcut path stay.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
github-actions Bot added a commit to omarcresp/t3code-flake that referenced this pull request Aug 19, 2026
## What's Changed
* fix(desktop): upgrade Clerk OAuth transport by @t3dotgg in pingdotgg/t3code#7479
* feat(server): run the background service on macOS via launchd by @t3dotgg in pingdotgg/t3code#6286
* fix(web): align sidebar statuses with project names by @RakshithBhat03 in pingdotgg/t3code#7491
* fix(desktop): close the window before quit cleanup by @t3dotgg in pingdotgg/t3code#6562


**Full Changelog**: pingdotgg/t3code@v0.0.34-nightly.20260819.1132...v0.0.34-nightly.20260819.1133

Upstream release: https://github.com/pingdotgg/t3code/releases/tag/v0.0.34-nightly.20260819.1133
frankdavidcorona pushed a commit to frankdavidcorona/t3code that referenced this pull request Aug 19, 2026
pandec added a commit to pandec/t3code that referenced this pull request Aug 19, 2026
Sync 5ea5a80..24c4ba6 (7 commits).

Zero conflicts and no fork delta in 22 of the 27 changed files, so the
work was the behavioral audit rather than the merge. Five targeted
reviews and two post-merge audits found nothing merge-introduced.

The notable landing is a macOS launchd background service (pingdotgg#6286).
It installs nothing on its own and leaves protocol 2, the trial
migration path, and every fork CLI command untouched, but a
LaunchAgent only runs while the user is logged in, and
pinnedRuntime still installs t3 from public npm, so
`t3 service install` on a fork machine would start upstream T3.
Reported rather than patched, along with upstream's own 200 ms
window after the command palette closes during which
isCommandPaletteOpen() still reports open and app shortcuts are
dead.

Model: Opus 5. Harness: Claude Code.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. 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