feat(server): run the background service on macOS via launchd - #6286
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 |
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: 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>
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>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
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 d3db140. Configure here.
| { | ||
| step: "stopping the installed launch agent", | ||
| command: "launchctl", | ||
| args: ["bootout", "--wait", serviceTarget], |
There was a problem hiding this comment.
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)
Reviewed by Cursor Bugbot for commit d3db140. Configure here.
…, 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>
## 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
…tgg#6286) Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.
|
@t3dotgg this breaks T3 Connect auth for me on ARM MacOS 26.5. Connect was setup and linked and status was green: npx t3@0.0.34-nightly.20260819.1133 connect statusThen, I ran It started T3 correctly, but Connect couldn't auth with cloudflared: <user>@<machine>:~ % ps aux | rg '[c]loudflared'
<user>@<machine>:~ % tail -n 200 ~/.t3/userdata/logs/boot-service.log | \
rg -i 'relay|cloudflared|tunnel|warning|error|failed'
cause: 'RelayAuthInvalidError: Relay authentication failed: not_authorized\n' +
' at AgentAwarenessRelay.publishThread (file:///Users/<user>/.t3/runtime/versions/0.0.34-nightly.20260819.1133/node_modules/t3/dist/bin.mjs:175637:6)'
[10:30:19.883] WARN (#248): agent activity publish failed
cause: 'RelayAuthInvalidError: Relay authentication failed: not_authorized\n' +
... |
* test(web): remove duplicate lookup assertion (pingdotgg#7364) * fix(mobile): show structured input option descriptions (pingdotgg#7321) * fix(orchestration): do not revive idle tasks from status-free progress (pingdotgg#7172) * refactor(server): simplify error transformation with Effect.mapError in GitHubPullRequestCli (pingdotgg#7385) Signed-off-by: aoright <102943475+aoright@users.noreply.github.com> * fix(preview): open local environment ports on localhost (pingdotgg#7300) * fix(desktop): prevent quit shortcut spillover (pingdotgg#7397) * fix(desktop): stop overwriting a custom dock icon on launch (pingdotgg#7125) * feat(web): show project location in new thread picker (pingdotgg#7392) Co-authored-by: shivam <91240327+shivamhwp@users.noreply.github.com> * fix(packaging): install AUR launcher icons where icon themes look (pingdotgg#7421) * fix(web): label pull request merge actions (pingdotgg#7381) * fix(server): avoid PRs inherited from default upstreams (pingdotgg#7317) * fix(desktop): stop the passkey dialog from popping as soon as sign-in opens (pingdotgg#7437) * feat(desktop): mute a browser tab (pingdotgg#7252) * fix(web): improve disconnected composer placeholder (pingdotgg#7122) Co-authored-by: t3-code[bot] <269035359+t3-code[bot]@users.noreply.github.com> Co-authored-by: maria <254055478+maria-rcks@users.noreply.github.com> * fix(desktop): throttle hidden preview rendering (pingdotgg#7445) Co-authored-by: t3-code[bot] <269035359+t3-code[bot]@users.noreply.github.com> Co-authored-by: maria <254055478+maria-rcks@users.noreply.github.com> * fix(server): stop probing Grok, Cursor, and OpenCode unless turned on (pingdotgg#7459) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * fix(desktop): boot the main window unthrottled so cold start paints at full speed (pingdotgg#7460) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * fix(threads): a merged PR settles its thread only once (pingdotgg#7454) * feat(cli): npx t3 triage hands broken installs to your own coding agent (pingdotgg#6563) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * fix(marketing): Safari gets the arm64 Mac download (pingdotgg#7473) * feat(web): add shortcuts to the surface dropdown (pingdotgg#7318) * fix(marketing): never serve the Intel build to Apple Silicon Macs (pingdotgg#7477) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * fix(web): animate command palette when closing (pingdotgg#5169) * fix(desktop): upgrade Clerk OAuth transport (pingdotgg#7479) * feat(server): run the background service on macOS via launchd (pingdotgg#6286) Co-authored-by: Claude Fable 5 <noreply@anthropic.com> * fix(web): align sidebar statuses with project names (pingdotgg#7491) Co-authored-by: GPT-5.6 <noreply@openai.com> * fix(desktop): close the window before quit cleanup (pingdotgg#6562) * fix(desktop): stop automatic passkey prompts (pingdotgg#7522) * docs(user): document phoenix triage and macOS background service The 2026-08-19 upstream sync added the triage command with no docs/user entry, and the docs index still called the background service Linux-only after launchd support landed. Found by PR #61 code review. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Signed-off-by: aoright <102943475+aoright@users.noreply.github.com> Co-authored-by: t3-code[bot] <269035359+t3-code[bot]@users.noreply.github.com> Co-authored-by: Nick Anisimov <n.anisimov.23@gmail.com> Co-authored-by: Maslin Edwin <maslinje@gmail.com> Co-authored-by: aoright <102943475+aoright@users.noreply.github.com> Co-authored-by: Guilherme Barros <gbarros1095@gmail.com> Co-authored-by: Bilal Bakr <62337003+Bil0000@users.noreply.github.com> Co-authored-by: Rishet11 <154429365+Rishet11@users.noreply.github.com> Co-authored-by: Wout Stiens <71498452+StiensWout@users.noreply.github.com> Co-authored-by: shivam <91240327+shivamhwp@users.noreply.github.com> Co-authored-by: Augie <augie@luebbers.email> Co-authored-by: Taras <Taras.Fomin@gmail.com> Co-authored-by: Gianmarco <gianmarcosimone89@gmail.com> Co-authored-by: Theo Browne <me@t3.gg> Co-authored-by: Chris Deeming <chris@xenforo.com> Co-authored-by: Inaya Yousfi <zied.essaber@gmail.com> Co-authored-by: maria <254055478+maria-rcks@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Rakshith Bhat <88523594+RakshithBhat03@users.noreply.github.com> Co-authored-by: GPT-5.6 <noreply@openai.com>
Range: 3b5d476..f2d5fc9 (21 upstream commits, merge-base 3b5d476). Shape: 23 files touched by both sides, 63 upstream-only, 525 fork-only. Zero conflicts — git found no textual overlap, so the audit below is the only thing that could have caught a bad merge. Conflicts: none. Every upstream-only file is byte-identical to origin/main and every fork-only file byte-identical to personal (verified by blob hash, not by inspection); only the 23 shared files were actually merged. Notable upstream changes landing on fork-rewritten files: - pingdotgg#7454 changed changeRequestAutoSettles from (state, autoSettleOnMerge) to (pr, {autoSettleOnMerge, thread}) so a merged PR settles its thread once. Both call sites live in fork-rewritten files (Sidebar.tsx +467 vs upstream +8, ChatView.tsx +550 vs upstream +36) and took the new shape. - pingdotgg#7318/pingdotgg#7252 rewrote RightPanelTabs.tsx (upstream 231/58 against the fork's 13/1). The fork's two "trustedFile" cases survived inside both surface switches — verified by reading the merged switches, since a line-level sweep cannot tell a surviving line from a reachable one. - pingdotgg#7459 turns Grok/Cursor/OpenCode probing off by default and folds the legacy in-config "enabled" flag into the envelope. Orthogonal to the fork's probe-cache pinning; unknown (fork) drivers default to enabled. - pingdotgg#7122 moved the disconnected composer placeholder into a shared constant. Its text is neutral about sending, so it does not contradict the fork's offline outbox. - pingdotgg#6286 adds launchd management under the label com.t3tools.t3code.service, distinct from this machine's hand-rolled com.t3code.server, so it cannot disturb the running install. - Clerk bump (pingdotgg#7522/pingdotgg#7479/pingdotgg#7437) rewrote the catalog and minimumReleaseAgeExclude blocks but not patchedDependencies. Invariants (docs/fork/README.md), all re-probed against the merged tree: 1. Migrations: 45 entries, ids unique and monotonic, max 46, id 34 still burned, filename numbers 033/037/038/039 still duplicated. PASS. 2. Fork-owned @effect/platform-node patch still pinned in patchedDependencies with its FORK-ONLY comment; patch file intact and carried in the lockfile by patch_hash. No effect bump in this range. PASS. 3. Sidebar default unflipped: AppSidebarLayout renders ThreadSidebar unless legacySidebarEnabled. Fork edits landed in Sidebar.tsx, the rendered one. PASS. 4. No fork-deleted line came back (sweep resurrected=0). PASS. 5. Upstream's "steers a running turn" ClaudeAdapter test still absent, with its explanatory comment. PASS. 6. Both project entry points still on the row: ellipsis opening the fork's project-actions dialog and gear navigating to /projects/$projectKey. PASS. Sweep (all three directions, 23 shared files): resurrected 0, dropped 0, fork-loss 0. Install: full pnpm install, 13 added / 7 removed, pnpm-lock.yaml unchanged afterwards — the lockfile committed here is the one the gate verified.


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/systemctlorchestration; 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.bootServiceis refactored around aBootServiceManagerabstraction: each platform supplies paths, a pure unit/plist renderer, and declarativelaunchctl/systemctlstep lists consumed by sharedinstall/uninstall/statusflows. macOS usesbootout --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 thegui/<uid>launchd domain.Onboarding and status copy no longer promise post-logout reachability on Mac: connect and
serviceprompts 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
launchdManagerinbootService.tsthat installs a launch agent plist, bootstraps it at login, and handles stop/restart/uninstall vialaunchctl.renderBootServicePlistto generate a launchd plist withRunAtLoad,KeepAlive,ThrottleInterval=5,ExitTimeOut=90, and consolidated stdout/stderr logging.selectBootServiceManagerabstraction, using a newHostProcessUserIdcontext for POSIX uid.Macroscope summarized d3db140.