Skip to content

fix(terminal): scope macOS daemons to login sessions - #9684

Closed
OrcaWin wants to merge 1 commit into
mainfrom
OrcaWin/issue-7936-macos-logout-orphaned-ptys
Closed

OrcaWin wants to merge 1 commit into
mainfrom
OrcaWin/issue-7936-macos-logout-orphaned-ptys

Conversation

@OrcaWin

@OrcaWin OrcaWin commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

Scopes macOS terminal daemons and PTYs to the login runtime that created them.

  • A full logout or changed login scope can no longer warm-attach to stale interactive PTYs.
  • Same-session app relaunch, normal quit/reopen, WindowServer recovery, concurrent clients, and returning through fast user switching preserve valid terminals.
  • Reboot and changed login scope use cold restore semantics.
  • macOS-only behavior stays behind runtime checks; SSH/headless macOS and Windows/Linux/WSL retain their existing behavior.

Closes #7936

Screenshots

No visual change.

Testing

  • pnpm lint — touched code is clean; the repository-wide command is blocked by the unchanged upstream non-exhaustive switch in src/renderer/src/components/skills/skill-freshness-group.tsx:101
  • pnpm typecheck
  • pnpm test — sanitized full suite: 3,141 files, 33,353 tests passed; 7 files/56 tests skipped
  • pnpm build
  • Added deterministic lifecycle integration and focused unit coverage — 13 files, 323 tests passed after rebase

AI Review Report

Ran four review rounds and resolved every valid finding. The review checked macOS lifecycle ownership, full logout, WindowServer recovery, app quit/crash and relaunch, reboot semantics, fast user switching, concurrent clients, reconnect and duplicate-daemon races, stale discovery state, mixed protocol versions, SSH/headless behavior, mobile/web clients, supported providers, and performance.

It explicitly checked cross-platform compatibility for macOS, Linux, Windows, and WSL, including runtime platform guards, paths, shell/process behavior, and Electron IPC boundaries. A valid finding identified a missing server route for closing startup query authority; the route was restored and covered by validation.

Security Audit

Daemon discovery and attachment are fenced by UID, macOS audit-session ID, and boot UUID. Cleanup verifies the exact PID and process start time before termination, uses bounded fixed-path launchctl and sysctl probes, and does not log terminal contents. The review also checked IPC authentication, stale metadata, mixed-version fail-closed behavior, multi-user/session isolation, command execution, and path handling. No dependencies or secret handling changed.

Notes

  • Safe deterministic fixtures model login-scope changes; no real developer logout, WindowServer kill, reboot, or unrelated Orca termination was performed.
  • Fast user switching preserves each still-valid login session independently.
  • WindowServer recovery preserves PTYs while the owning login scope remains valid.
  • X handle: N/A

@coderabbitai

coderabbitai Bot commented Jul 21, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds macOS GUI login-session runtime scopes to daemon handshakes, health checks, startup, adoption, replacement, cleanup, restart, and PTY history. Scope values are resolved from process audit identity and boot-session UUID, persisted for history isolation, and passed through daemon CLI and adapter configuration. Mismatched clients and legacy daemons are rejected or retired. New tests cover parsing, scoped recovery, login transitions, reboot, account isolation, concurrency, and daemon replacement. Documentation and a reliability gate define validation scenarios and assertions.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: scoping macOS daemons to login sessions.
Description check ✅ Passed The description matches the template and fills all required sections with relevant details.
Linked Issues check ✅ Passed The changes address #7936 by scoping daemons/PTys to login sessions and preserving valid same-session and fast-switch behavior.
Out of Scope Changes check ✅ Passed The changes are focused on macOS login-session scoping, related tests, docs, and protocol plumbing with no clear unrelated additions.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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.

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

🧹 Nitpick comments (2)
src/main/daemon/daemon-pty-adapter.ts (1)

1234-1245: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Simplify redundant ternary.

Since getMacDaemonSystemResolverHealth accepts an optional fourth runtimeScope parameter, you can pass this.runtimeScope directly and remove the ternary. Passing undefined yields the exact same behavior as omitting the argument.

♻️ Proposed refactor
-    const health = this.runtimeScope
-      ? await getMacDaemonSystemResolverHealth(
-          this.socketPath,
-          this.tokenPath,
-          this.protocolVersion,
-          this.runtimeScope
-        )
-      : await getMacDaemonSystemResolverHealth(
-          this.socketPath,
-          this.tokenPath,
-          this.protocolVersion
-        )
+    const health = await getMacDaemonSystemResolverHealth(
+      this.socketPath,
+      this.tokenPath,
+      this.protocolVersion,
+      this.runtimeScope
+    )
config/reliability-gates.jsonc (1)

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

Test-file references for the new gate are incomplete/inconsistent across the two documentation artifacts. The gate declares 5 testFiles/commands entries but assertionRefs documents only 2 of them, and the reproduction doc's headline Run: command lists only 3 of the 5, even though it separately cites daemon-init.test.ts assertions.

  • config/reliability-gates.jsonc#L5487-5576: add assertionRefs entries for daemon-login-session-history.test.ts, macos-gui-session-scope.test.ts, and daemon-health.test.ts to match the pattern used by every other gate in this file (one entry per testFiles item).
  • docs/bug-reproductions/issue-7936-macos-login-session-daemon.md#L9-14: extend the Run: command to include daemon-health.test.ts and daemon-init.test.ts so it matches the gate's registered commands and the daemon-init.test.ts assertions discussed later in the same doc (line 28).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 871c423a-45c9-4150-8bb9-5a2b3e6978e6

📥 Commits

Reviewing files that changed from the base of the PR and between 1293e1c and bd143ab.

📒 Files selected for processing (22)
  • config/reliability-gates.jsonc
  • docs/bug-reproductions/issue-7936-macos-login-session-daemon.md
  • src/main/daemon/client.ts
  • src/main/daemon/daemon-entry.test.ts
  • src/main/daemon/daemon-entry.ts
  • src/main/daemon/daemon-health.test.ts
  • src/main/daemon/daemon-health.ts
  • src/main/daemon/daemon-hello-protocol.ts
  • src/main/daemon/daemon-init.test.ts
  • src/main/daemon/daemon-init.ts
  • src/main/daemon/daemon-login-session-history.test.ts
  • src/main/daemon/daemon-login-session-history.ts
  • src/main/daemon/daemon-main.ts
  • src/main/daemon/daemon-protocol-version.ts
  • src/main/daemon/daemon-pty-adapter.ts
  • src/main/daemon/daemon-pty-provider.ts
  • src/main/daemon/daemon-server.ts
  • src/main/daemon/macos-gui-session-scope.test.ts
  • src/main/daemon/macos-gui-session-scope.ts
  • src/main/daemon/macos-login-session-lifecycle.test.ts
  • src/main/daemon/types.ts
  • src/main/index.ts

@OrcaWin
OrcaWin force-pushed the OrcaWin/issue-7936-macos-logout-orphaned-ptys branch from bd143ab to 2b705a7 Compare July 21, 2026 20:25

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 78c086b1-5be9-4762-84ca-acb682d78512

📥 Commits

Reviewing files that changed from the base of the PR and between bd143ab and 2b705a7.

📒 Files selected for processing (22)
  • config/reliability-gates.jsonc
  • docs/bug-reproductions/issue-7936-macos-login-session-daemon.md
  • src/main/daemon/client.ts
  • src/main/daemon/daemon-entry.test.ts
  • src/main/daemon/daemon-entry.ts
  • src/main/daemon/daemon-health.test.ts
  • src/main/daemon/daemon-health.ts
  • src/main/daemon/daemon-hello-protocol.ts
  • src/main/daemon/daemon-init.test.ts
  • src/main/daemon/daemon-init.ts
  • src/main/daemon/daemon-login-session-history.test.ts
  • src/main/daemon/daemon-login-session-history.ts
  • src/main/daemon/daemon-main.ts
  • src/main/daemon/daemon-protocol-version.ts
  • src/main/daemon/daemon-pty-adapter.ts
  • src/main/daemon/daemon-pty-provider.ts
  • src/main/daemon/daemon-server.ts
  • src/main/daemon/macos-gui-session-scope.test.ts
  • src/main/daemon/macos-gui-session-scope.ts
  • src/main/daemon/macos-login-session-lifecycle.test.ts
  • src/main/daemon/types.ts
  • src/main/index.ts
🚧 Files skipped from review as they are similar to previous changes (20)
  • src/main/daemon/daemon-entry.test.ts
  • src/main/daemon/daemon-health.test.ts
  • docs/bug-reproductions/issue-7936-macos-login-session-daemon.md
  • src/main/daemon/daemon-login-session-history.test.ts
  • src/main/daemon/daemon-main.ts
  • src/main/index.ts
  • src/main/daemon/daemon-login-session-history.ts
  • src/main/daemon/daemon-hello-protocol.ts
  • src/main/daemon/daemon-pty-provider.ts
  • src/main/daemon/daemon-server.ts
  • src/main/daemon/daemon-protocol-version.ts
  • src/main/daemon/macos-gui-session-scope.test.ts
  • src/main/daemon/client.ts
  • src/main/daemon/types.ts
  • src/main/daemon/daemon-entry.ts
  • src/main/daemon/daemon-health.ts
  • src/main/daemon/macos-login-session-lifecycle.test.ts
  • src/main/daemon/daemon-pty-adapter.ts
  • src/main/daemon/daemon-init.test.ts
  • src/main/daemon/daemon-init.ts

Comment on lines +54 to +70
async function readLaunchctlProcessDomain(pid: number): Promise<string> {
const { execFile } = await import('node:child_process')
return new Promise((resolve, reject) => {
execFile(
'/bin/launchctl',
['print', `pid/${pid}`],
{ encoding: 'utf8', timeout: 1_000, maxBuffer: 64 * 1024 },
(error, stdout) => {
if (error) {
reject(error)
} else {
resolve(stdout)
}
}
)
})
}

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- file map ---'
git ls-files 'src/main/daemon/macos-gui-session-scope.ts' 'src/main/daemon/*' | sed -n '1,120p'

printf '\n%s\n' '--- outline ---'
ast-grep outline src/main/daemon/macos-gui-session-scope.ts --view expanded

printf '\n%s\n' '--- relevant source lines ---'
nl -ba src/main/daemon/macos-gui-session-scope.ts | sed -n '1,220p'

printf '\n%s\n' '--- search for parser/consumer ---'
rg -n "parseMacosProcessSessionIdentity|resolveMacosGuiSessionScope|readLaunchctlProcessDomain|launchctl print|pid/" src/main/daemon src -g '!**/dist/**' -g '!**/build/**'

Repository: stablyai/orca

Length of output: 6381


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- source: macos-gui-session-scope.ts ---'
sed -n '1,220p' src/main/daemon/macos-gui-session-scope.ts

printf '\n%s\n' '--- tests: macos-gui-session-scope.test.ts ---'
sed -n '1,260p' src/main/daemon/macos-gui-session-scope.test.ts

printf '\n%s\n' '--- call sites ---'
rg -n "readMacosProcessSessionIdentity|resolveMacosGuiSessionScope|parseMacosProcessSessionIdentity|parseMacosGuiSessionScope|readLaunchctlProcessDomain|readMacosBootSessionId" src/main/daemon -g '!**/dist/**' -g '!**/build/**'

Repository: stablyai/orca

Length of output: 12671


🌐 Web query:

launchctl print output documented machine-readable stable contract macOS audit session security context

💡 Result:

The output of the launchctl print command on macOS is not a stable, machine-readable contract [1][2][3]. According to the official launchctl(1) manual, the output is intended solely for human inspection during debugging and profiling [4][1][3]. Apple explicitly warns that the output format is not an API and does not provide any guarantees regarding forward compatibility [1][2][3]. The structure, content, and formatting of the output may change at any time across macOS releases without warning [1][2][3]. Consequently, it is strongly advised against relying on parsing this output in production scenarios or automated scripts [1][3]. While some third-party tools exist to parse this data into structured formats like JSON for ad-hoc automation, they are inherently fragile and subject to breaking whenever Apple modifies the launchctl command output [5].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- daemon-init excerpt ---'
sed -n '1080,1135p' src/main/daemon/daemon-init.ts

printf '\n%s\n' '--- related error handling ---'
rg -n "Cannot determine the macOS GUI login-session owner|resolveMacosGuiSessionScope|runtimeScope|readMacosProcessSessionIdentity" src/main/daemon/daemon-init.ts src/main/daemon -g '!**/dist/**' -g '!**/build/**'

Repository: stablyai/orca

Length of output: 16522


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- daemon-init around startup/resolver ---'
sed -n '720,790p' src/main/daemon/daemon-init.ts

printf '\n%s\n' '--- daemon-init around GUI startup caller ---'
sed -n '1118,1185p' src/main/daemon/daemon-init.ts

printf '\n%s\n' '--- macos-gui-session-scope exact lines ---'
sed -n '1,170p' src/main/daemon/macos-gui-session-scope.ts

Repository: stablyai/orca

Length of output: 9708


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- resolveMacosGuiSessionScope references ---'
rg -n "resolveMacosGuiSessionScope\s*\(" src

printf '\n%s\n' '--- readMacosProcessSessionIdentity references ---'
rg -n "readMacosProcessSessionIdentity\s*\(" src

printf '\n%s\n' '--- launchctl print references ---'
rg -n "launchctl'\s*,\s*\['print'|launchctl print|pid/" src/main/daemon src -g '!**/dist/**' -g '!**/build/**'

Repository: stablyai/orca

Length of output: 4146


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '630,700p' src/main/index.ts

Repository: stablyai/orca

Length of output: 3917


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "function startFirstWindowStartupServices|startFirstWindowStartupServices\(" src/main/index.ts src/main -g '!**/dist/**' -g '!**/build/**'
sed -n '1,220p' src/main/index.ts

Repository: stablyai/orca

Length of output: 12648


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,240p' src/main/startup/first-window-startup-services.ts

Repository: stablyai/orca

Length of output: 4046


Avoid relying on launchctl print as the only session source. Its output is undocumented and can change; if that happens, GUI-session detection will fail over to the local PTY fallback instead of preserving daemon-backed sessions. Add a compatibility fallback or a more stable source for the audit-session lookup.

@brennanb2025

Copy link
Copy Markdown
Contributor

Reviewed this after dragonfire1119's live test of v1.4.149-rc.1.issue7936 failed (Discord): a terminal created post-update, then logout/login, still produced login: / Login incorrect, and only a manual daemon restart recovered. I verified the RC tag contains this PR's commit, so the miss is behavioral. Findings from a code walk of the shipped tag:

1. checkDaemonHealth / getMacDaemonSystemResolverHealth never verify the response scope. DaemonClient.ensureConnected() rejects when response.runtimeScope !== this.runtimeScope, but the two raw-socket probes only check ok. A pre-scope protocol-25 daemon (v1.4.150-rc.0 is protocol 25 without this PR — this exact collision is live in the field) answers hello fine → 'healthy', while every scoped RPC client fails against it → getAliveDaemonSessionCount returns null → both "can't verify live sessions" branches preserve it. Result: a stale daemon the app can't even talk to is preserved forever, and the adapter limps to local fallback. The doc's claim that "a pre-scope daemon is adopted only after its verified process audit session matches" is implemented only for legacy-protocol daemons (legacyDaemonMatchesRuntimeScope), not for same-protocol pre-scope daemons on the current socket.

2. resolveMacosGuiSessionScope throws → no daemon at all. In startDaemonPtyProvider the throw propagates to onDaemonError → silent local-PTY fallback, and the stale daemon is left untouched. The 1s launchctl/sysctl timeouts are most likely to fire exactly during the post-login storm — i.e., the moment this fix must work. A resolution failure should degrade to unscoped-init (old behavior), not to daemon-less.

3. Unscoped orca serve clients vs. scoped daemons = mutual replacement loop. Serve is unscoped by design; its hello to a scoped daemon hits hello.runtimeScope !== this.runtimeScope → 'rejected' → serve's launcher treats it as never-adoptable and replaces it; the GUI client then rejects serve's unscoped daemon and replaces it back. Anyone running desktop + serve on one macOS box gets a daemon kill war, each cycle destroying all PTYs. Suggest: reject only when the client's scope is present and different; tolerate scope-absent clients (they're uid+token-fenced already).

4. createLegacyDaemonAdapters passes historyPath: getHistoryDir() (unscoped) instead of getHistoryDir(runtimeScope) — inconsistent with the new scoped-slot model.

Also, the symptom itself lives below this PR's layer: the #9301 preflight caches its birth-time 'accepted' verdict per daemon process, so a surviving dead-session daemon wraps every spawn in login(1) forever — that's the direct generator of the login: prompt zombies, and no startup-time fence can heal a daemon the app never restarts against. I've opened #9826, which makes GUI daemons detect login-session death from the inside (cache-bypassing PAM probe + resolver corroboration) and self-retire crash-style so the next start cold-restores. It complements this PR rather than replacing it.

To confirm which link broke on the reporter's machine, their ~/Library/Application Support/orca/logs/daemon.log from a broken state would settle it — this PR's client-hello-rejected/client-hello-accepted records are exactly the discriminator.

@brennanb2025

Copy link
Copy Markdown
Contributor

Data point for the pre-scope adoption guard from a live dead-session experiment (details on #9826): after a real audit-session teardown (SSH-minted session, orphan survived, reparented to launchd), launchctl print pid/<orphan> continues to report the dead session's original asid with a parseable security context. So readMacosProcessSessionIdentity on a post-logout daemon should reliably see the stale asid and fence it — the guard's input behaves as designed. The gap remains the paths that never consult that guard (current-protocol pre-scope daemons, finding 1 in my earlier comment).

@brennanb2025

Copy link
Copy Markdown
Contributor

Closing as superseded by #9826 after discussion with Brennan.

Rationale: the live RC test (v1.4.149-rc.1.issue7936) was falsified in the field by the reporter, and the code walk above found four integration holes in the startup-time fence approach — most critically the preserve-deadlock against pre-scope protocol-25 daemons (which 1.4.150-rc.0 ships) and the unscoped-serve vs scoped-daemon replacement war. More fundamentally, a startup-time fence can only help when the app restarts and every adopt-vs-replace decision goes right; the surviving daemon itself stays broken.

#9826 inverts the approach: the daemon detects its login session died from the inside (PAM probe + resolver corroboration, hang-shaped fallback trigger) and retires itself, so the next start cold-starts a fresh daemon regardless of any client-side decision — validated with live lifecycle QA and real dead-session experiments (including the launchctl stale-asid data point posted above, which confirms this PR's process-identity guard input was sound).

What remains valuable from this PR's scope if we revisit later: the warm-attach fence for fast-user-switching correctness and multi-session isolation, and the login-scoped history slot. If that gets picked back up, the four findings above are the pre-merge checklist. #7936 stays open until #9826 is field-confirmed.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] macOS logout leaves orphaned PTYs → broken "Login incorrect" terminals on next login, needs reboot

2 participants