Skip to content

feat(connect): keep macOS hosts reachable after reboot - #4960

Closed
t3dotgg wants to merge 4 commits into
mainfrom
t3code/macos-connect-launch-agent
Closed

feat(connect): keep macOS hosts reachable after reboot#4960
t3dotgg wants to merge 4 commits into
mainfrom
t3code/macos-connect-launch-agent

Conversation

@t3dotgg

@t3dotgg t3dotgg commented Jul 30, 2026

Copy link
Copy Markdown
Member

Bare npx t3 connect could link a Mac, but it could not leave behind a durable server. The existing background-service path only supported Linux, so the connection disappeared when the CLI exited and did not return after login or reboot.

This adds a per-user macOS LaunchAgent to the existing service lifecycle. Connect pins the exact running T3 version, starts it through a stable runtime path, and launchd keeps it alive and loads it again after login. Remote updates atomically switch that stable path and restart the same LaunchAgent, with rollback if launchd rejects the restart. Linux systemd behavior is unchanged.

The LaunchAgent preserves the setup PATH for provider discovery, reports the macOS background-item setting when activation is blocked, and is covered by focused lifecycle, rollback, capability, updater, CLI, and documentation tests.

Verification:

  • vp test run apps/server/src/cloud/bootService.test.ts apps/server/src/cloud/selfUpdate.test.ts apps/server/src/cli/service.test.ts apps/server/src/cli/connect.test.ts apps/server/src/bin.test.ts
  • vp run --filter @t3tools/shared --filter @t3tools/contracts --filter t3 typecheck
  • targeted vp lint on changed TypeScript files

Generated by GPT-5.6-sol using the Codex harness in T3 Code.


Note

Medium Risk
Changes host lifecycle (launchctl, symlinks, rollback) and remote self-update on macOS; Linux paths are largely preserved but shared install/status code grew. Mistakes could leave a broken background service or failed rollback.

Overview
Adds macOS launchd alongside Linux systemd so T3 Code can stay reachable after login and reboot, not only on Linux.

Boot service: BootService installs a per-user LaunchAgent (com.t3tools.t3code.server), pins the CLI into a managed runtime, and runs serve through a stable symlink at runtime/service/current so remote updates can swap versions without rewriting the plist. Install/update uses launchctl (enable, bootstrap, bootout) with rollback on failure; status checks plist, symlink, loaded state, and Login Items disablement. macOS always pins the exact CLI version (Linux can still reuse stable global/checkout entries). writeSymbolicLinkAtomically mirrors atomic file writes for symlink switches.

Self-update & connect: macOS LaunchAgent processes advertise boot-service; updates switch the runtime symlink and kickstart -k, with rollback if launchd rejects the restart. HostProcessUserId supplies the GUI domain for launchctl. CLI/docs messaging now covers macOS + Linux and softer “start automatically in the background” wording.

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

Note

Add macOS launchd support to keep hosts reachable after reboot

  • Implements a per-user macOS LaunchAgent in bootService.ts: writes a plist, manages an active runtime symlink atomically, and starts/stops/uninstalls the service via launchctl.
  • Self-update on macOS switches the runtime symlink and restarts the job via launchctl kickstart; on failure it restores the previous symlink target.
  • Adds writeSymbolicLinkAtomically in atomicWrite.ts and HostProcessUserId in hostProcess.ts to support atomic link management and per-user launchd targets.
  • Updates status output, onboarding prompts, and user-facing docs to reflect macOS launchd alongside Linux systemd.
  • Risk: macOS boot-service install and rollback involve several sequential launchctl steps; partial failures trigger a rollback that restores the previous plist, symlink, and loaded state.

Macroscope summarized b3a3007.

Summary by CodeRabbit

  • New Features

    • Added background-service support on macOS alongside Linux.
    • T3 Code can now start automatically and remain available in the background on supported systems.
    • macOS service installation, status checks, updates, rollback, and uninstall are supported.
  • Bug Fixes

    • Improved self-updates for macOS-managed services, including recovery when a restart fails.
    • Added safer runtime switching to help preserve service availability.
  • Documentation

    • Updated setup and lifecycle guidance for macOS and Linux background services.

@coderabbitai

coderabbitai Bot commented Jul 30, 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: 9102020a-7799-407a-8b9f-5f1f7607ec24

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:

  • ✅ Review completed - (🔄 Check again to review again)

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 Jul 30, 2026

@macroscopeapp macroscopeapp 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.

One finding: BootServiceCommandError.message now selects caller-visible guidance by substring-matching the free-form step attribute. Everything else in the touched Effect service code (inline Context.Service interfaces, make/layer exports, environment-acquired dependencies, Schema.TaggedErrorClass failures with preserved cause, namespaced effect/* imports) follows the conventions.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/server/src/cloud/bootService.ts Outdated
Comment thread apps/server/src/cloud/bootService.ts Outdated
Comment thread apps/server/src/cloud/bootService.ts
@github-actions github-actions Bot added size:XL 500-999 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Jul 30, 2026

@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 2 potential issues.

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

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

macroscopeapp Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR adds macOS LaunchAgent support, a new platform capability with launchd integration for service lifecycle management (install, uninstall, restart, rollback). The scope includes system-level integration and new runtime behavior for auto-starting services after reboot, warranting human review.

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

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
apps/server/src/cloud/bootService.test.ts (1)

66-78: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for a darwin session with no user ID.

provideHostRefs always supplies HostProcessUserId as 501. bootService.ts treats a null user ID on darwin as unsupported at Lines 331-335 and returns an unsupported status at Lines 730-736. No test exercises that branch.

Make the user ID configurable, then assert BootServiceUnsupportedError and supported: false for darwin with a null user ID.

As per coding guidelines: "Backend behavior changes must include focused tests for that behavior."

💚 Proposed parameter change
 const provideHostRefs = (
   home: string,
   platform: NodeJS.Platform = "linux",
   pathEnvironment = "/test/bin:/usr/bin:/bin",
+  userId: number | null = 501,
 ) =>
   Effect.provide(
     Layer.mergeAll(
       Layer.succeed(HostProcessPlatform, platform),
       Layer.succeed(HostProcessEnvironment, { HOME: home, PATH: pathEnvironment }),
-      Layer.succeed(HostProcessUserId, 501),
+      Layer.succeed(HostProcessUserId, userId),
       ConfigProvider.layer(ConfigProvider.fromEnv({ env: { HOME: home } })),
     ),
   );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/cloud/bootService.test.ts` around lines 66 - 78, Update
provideHostRefs to accept a configurable user ID, including null, while
preserving 501 as the default for existing tests. Add focused coverage for a
darwin session using a null user ID, asserting that the boot flow raises
BootServiceUnsupportedError and returns supported: false.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/server/src/cli/service.ts`:
- Line 54: The service status hint in apps/server/src/cli/service.ts lines 54-54
should use the documented npx t3@latest service install invocation. Update the
uninstall command in docs/user/background-service.md lines 46-47 to use npx
t3@latest service uninstall, keeping service management commands consistent.

In `@apps/server/src/cloud/bootService.ts`:
- Around line 423-434: Update the disabled-state parsing in
readLaunchAgentDisabled and the macOS status case in
apps/server/src/cloud/bootService.ts: recognize both “=> true” and “=> disabled”
forms while preserving the existing service-label match. Update the fixture in
apps/server/src/cloud/bootService.test.ts:554-557 to cover both boolean
spellings.

In `@docs/architecture/server-updates.md`:
- Around line 35-40: Run the Markdown formatter using `vp check --fix` on the
updated table, ensuring the separator and body rows have normalized column
widths and consistent trailing padding. Preserve the table content and commit
the formatter-clean result.

In `@docs/user/background-service.md`:
- Around line 9-11: Update the reboot behavior wording in the background setup
documentation to state that T3 Code “starts again automatically after a reboot,”
replacing the ambiguous “returns automatically” phrasing while preserving the
surrounding platform-specific startup details.

---

Nitpick comments:
In `@apps/server/src/cloud/bootService.test.ts`:
- Around line 66-78: Update provideHostRefs to accept a configurable user ID,
including null, while preserving 501 as the default for existing tests. Add
focused coverage for a darwin session using a null user ID, asserting that the
boot flow raises BootServiceUnsupportedError and returns supported: false.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8a005863-11da-44bd-af41-6af8dd2de975

📥 Commits

Reviewing files that changed from the base of the PR and between 90f3913 and b3a3007.

📒 Files selected for processing (12)
  • apps/server/src/atomicWrite.ts
  • apps/server/src/cli/connect.ts
  • apps/server/src/cli/service.test.ts
  • apps/server/src/cli/service.ts
  • apps/server/src/cloud/bootService.test.ts
  • apps/server/src/cloud/bootService.ts
  • apps/server/src/cloud/selfUpdate.test.ts
  • apps/server/src/cloud/selfUpdate.ts
  • docs/architecture/server-updates.md
  • docs/user/background-service.md
  • packages/contracts/src/environment.ts
  • packages/shared/src/hostProcess.ts

return "T3 Code service\n Status: unavailable on this machine\n Supported on: macOS with launchd or Linux with systemd";
}
if (!status.installed) {
return "T3 Code service\n Status: not installed\n Next: Run `t3 service install`.";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use one command invocation for service management.

The CLI status hint and documentation use different executables. Users who use the documented npx flow may not have a bare t3 command.

  • apps/server/src/cli/service.ts#L54-L54: use npx t3@latest service install.
  • docs/user/background-service.md#L46-L47: use npx t3@latest service uninstall.
📍 Affects 2 files
  • apps/server/src/cli/service.ts#L54-L54 (this comment)
  • docs/user/background-service.md#L46-L47
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/cli/service.ts` at line 54, The service status hint in
apps/server/src/cli/service.ts lines 54-54 should use the documented npx
t3@latest service install invocation. Update the uninstall command in
docs/user/background-service.md lines 46-47 to use npx t3@latest service
uninstall, keeping service management commands consistent.

Comment on lines +423 to +434
Effect.map((result) =>
result.code === 0
? Option.some(
result.stdout
.split("\n")
.some(
(line) =>
line.includes(BOOT_SERVICE_LAUNCH_AGENT_LABEL) && line.includes("=> true"),
),
)
: Option.none<boolean>(),
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

launchctl print-disabled output format "=> disabled" macOS Sonoma

💡 Result:

On macOS Sonoma and other modern versions of macOS, the output format of the launchctl print-disabled command is not a formal API and is not guaranteed to remain stable across OS releases [1][2][3]. However, in practice, the output follows a structured key-value pattern for services within a specified domain [4]. The command outputs a block of text starting with a header such as disabled services = {, followed by a list of service labels mapped to a boolean value [4]. The format generally appears as: "service.label.name" => boolean In this mapping, the boolean value indicates the disabled state: - true: The service is disabled [4][5]. - false: The service is enabled [6][4]. Example output: disabled services = { "com.example.service1" => true "com.example.service2" => false } Because this output format is intended for human diagnostic use rather than programmatic parsing, Apple explicitly warns against relying on its structure in production scripts or automation [1][2][3]. Developers attempting to parse this output should be aware that it may change without warning and that relying on specific regex patterns may break after system updates [6]. If you need to check for specific disabled services, it is recommended to use the output for human inspection or to use more stable management subcommands when possible [7][8].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate files =="
git ls-files 'apps/server/src/cloud/bootService.ts' 'apps/server/src/cloud/bootService.test.ts' | sed -n '1,20p'

echo
echo "== relevant bootService snippets =="
sed -n '400,445p' apps/server/src/cloud/bootService.ts
echo
sed -n '1,80p' apps/server/src/cloud/bootService.ts

echo
echo "== relevant bootService.test snippet =="
sed -n '535,565p' apps/server/src/cloud/bootService.test.ts

echo
echo "== readLaunchAgentDisabled usages =="
rg -n "readLaunchAgentDisabled|launchctl print-disabled|print-disabled|rollbackFailedInstall|status" apps/server/src/cloud/bootService.ts apps/server/src/cloud/bootService.test.ts

Repository: pingdotgg/t3code

Length of output: 9698


Make the disabled-state parser accept the possible launchctl print-disabled boolean forms. readLaunchAgentDisabled and the macOS status case only accept "service" => true; change the parser to also accept "service" => disabled, and update the fixture to cover both spellings so the parser is not tied to one observed output.

📍 Affects 2 files
  • apps/server/src/cloud/bootService.ts#L423-L434 (this comment)
  • apps/server/src/cloud/bootService.test.ts#L554-L557
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/cloud/bootService.ts` around lines 423 - 434, Update the
disabled-state parsing in readLaunchAgentDisabled and the macOS status case in
apps/server/src/cloud/bootService.ts: recognize both “=> true” and “=> disabled”
forms while preserving the existing service-label match. Update the fixture in
apps/server/src/cloud/bootService.test.ts:554-557 to cover both boolean
spellings.

Comment on lines +35 to +40
| Advertised value | Process shape | Client behavior |
| ----------------- | --------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| `boot-service` | Server running under the T3-managed systemd user service or macOS LaunchAgent | Call the update RPC; the managed service switches runtime and restarts. |
| `respawn` | Published npm CLI running in the foreground on macOS or Linux | Call the update RPC; the process hands off to a detached replacement. |
| `desktop-managed` | Backend supervised by the desktop app | Tell the user to update the desktop app on the server machine. |
| absent | Older server, development checkout, Windows foreground process, or an unrecognized supervisor | Offer the exact manual relaunch command. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Run the Markdown formatter on the updated table.

The separator row at Line 36 is wider than the header row at Line 35, and the body rows use inconsistent trailing padding. The table still renders, but the column widths are not normalized.

Run vp check --fix and commit the result.

As per coding guidelines: "Markdown edits must be formatter-clean; run vp check --fix before committing."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/architecture/server-updates.md` around lines 35 - 40, Run the Markdown
formatter using `vp check --fix` on the updated table, ensuring the separator
and body rows have normalized column widths and consistent trailing padding.
Preserve the table content and commit the formatter-clean result.

Source: Coding guidelines

Comment on lines +9 to +11
After T3 Connect links the machine, accept the default background setup. T3 Code starts immediately
and returns automatically after a reboot. On macOS, the per-user LaunchAgent starts after you sign
in. On Linux, the systemd user service starts at boot and keeps running after you log out.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Clarify the reboot behavior.

“Returns automatically after a reboot” does not clearly state that the service starts again. Replace it with “starts again automatically after a reboot.”

Proposed wording
- and returns automatically after a reboot.
+ and starts again automatically after a reboot.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
After T3 Connect links the machine, accept the default background setup. T3 Code starts immediately
and returns automatically after a reboot. On macOS, the per-user LaunchAgent starts after you sign
in. On Linux, the systemd user service starts at boot and keeps running after you log out.
After T3 Connect links the machine, accept the default background setup. T3 Code starts immediately
and starts again automatically after a reboot. On macOS, the per-user LaunchAgent starts after you sign
in. On Linux, the systemd user service starts at boot and keeps running after you log out.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/user/background-service.md` around lines 9 - 11, Update the reboot
behavior wording in the background setup documentation to state that T3 Code
“starts again automatically after a reboot,” replacing the ambiguous “returns
automatically” phrasing while preserving the surrounding platform-specific
startup details.

@t3dotgg

t3dotgg commented Aug 19, 2026

Copy link
Copy Markdown
Member Author

Note

🤖 Fable 5 responding on behalf of Theo

Closing: #6286 shipped the macOS LaunchAgent and this branch cannot be rebased onto it.

Verified, not judgment: both PRs rewrite the same bootService.ts install/uninstall/status flow and add HostProcessUserId to packages/shared/src/hostProcess.ts with incompatible types, so there is no merge order that works. This branch also targets update machinery main replaced after it forked (the symlink-switching path in selfUpdate.ts is gone; main uses the stable launcher plus service-state.json), and it edits docs/architecture/server-updates.md, which no longer exists.

Judgment: #6286 is also the better shape. No platform branches in the lifecycle flow, PATH handled at server startup by fixPath instead of baked into the plist, and honest login-not-boot copy. One thing from here worth carrying forward as a follow-up: this PR snapshotted the Login Items enabled state before running launchctl enable; merged main re-enables unconditionally on every activate.

@t3dotgg t3dotgg closed this Aug 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL 500-999 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