Skip to content

perf(process-table): name macOS terminals from tdev= instead of ps tty= - #24890

Closed
giorgio-zamparelli wants to merge 1 commit into
stablyai:mainfrom
giorgio-zamparelli:perf/macos-ps-tdev
Closed

giorgio-zamparelli wants to merge 1 commit into
stablyai:mainfrom
giorgio-zamparelli:perf/macos-ps-tdev

Conversation

@giorgio-zamparelli

Copy link
Copy Markdown
Contributor

ELI5

Orca regularly asks macOS for the list of every running process, including which terminal each one belongs to. Asking for the terminal's name makes macOS search its device folder once per process, which on a Mac with many processes and terminals takes over a second of CPU every time, and Orca asks every couple of seconds. This PR asks for the terminal's number instead, which is instant, and looks the names up itself once, so Orca sees exactly the same names at a small fraction of the cost.

What Changed

  • PS_ARGS on macOS asks ps for tdev= (major/minor) instead of tty=. Linux is unchanged.
  • New src/shared/darwin-terminal-names.ts turns each major/minor back into the exact name ps -o tty= prints (16/79 becomes ttys079, ?? stays ??). It keeps a /dev index of tty* and console entries, built on first use and rebuilt only when a capture names a terminal it has not seen (one opened since). A device a fresh walk still cannot name (a process outliving its terminal node) keeps its major/minor spelling and is remembered, so it does not cost a walk per capture.
  • process-table-snapshot-reader.ts applies it to the full capture on macOS. Naming is best effort: if /dev cannot be read, the capture keeps its major/minor spellings rather than turning a readable table into a failed one.
  • shell-foreground-snapshot.test.ts recognised the full capture by tty= in its argv; PS_ARGS follows the real host, so on a macOS host it now matches PS_ARGS itself.

Every consumer of row.tty (agent-foreground-process-remote-evidence.ts, spawn-file-foreground-process.ts, spawn-file-child-processes.ts, the posix fence) receives identical strings.

Why

Measured on macOS 26.6.2, Apple M5 Max, 1,854 processes and 143 ptys (#24889):

  • ps -axo pid=,ppid=,pgid=,tpgid=,stat=,tty=,lstart=,command=: 1.5–1.8 CPU-seconds per run, 1.3 s of it system time spent naming terminals; 6.6 s wall at load ~67.
  • the same columns with tdev=: 0.19 CPU-seconds.
  • the daemon ran this capture 14 times in 20 s here, because the 500 ms TTL starts after a ~2 s capture. That is about one core, and every capture lands past PROCESS_TABLE_EVIDENCE_BUDGET_MS (1.2 s).

tty= is slow because devname(3) walks /dev for every process, so the cost grows with processes times /dev entries. The existing comment already measured tty= as 0.29 s of a 0.34 s capture; with more terminals open it dominates.

Alternatives considered: dropping the terminal column (consumers need it to tell foreground groups on one terminal apart); naming only pane roots (changes the row contract every consumer relies on); generating /var/run/dev.db with dev_mkdb (needs root, per machine, and macOS does not maintain it).

Linked Issue

Fixes #24889

Visual Proof

N/A: no UI or interaction change. This changes how the daemon reads the macOS process table, not what Orca shows.

Testing

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

  • New darwin-terminal-names.test.ts (9 tests): the translation rewrites only the device column (an argv containing 1/2 is untouched), keeps unknown devices as major/minor, leaves legacy row shapes alone; the index stats only tty*/console, lets the first entry win, rebuilds for a newly opened terminal, walks only once for a vanished one; the reader skips /dev for captures without terminals and falls back cleanly when /dev is unreadable.

  • Real host, macOS 26.6.2: a throwaway test (not committed) ran the real reader against ps -o tty=. It found 0 mismatches over 1,969 processes, 664 of them with a terminal; capture 356 ms against 6,559 ms for the old argv under the same load.

  • vitest run src/shared src/main/pty src/main/providers src/main/daemon: 1,221 files pass. The 4 tests in 3 files that fail on this machine fail identically on unmodified main: node-pty-fd-leak (native module not built, I installed with --ignore-scripts), local-pty-provider-spawn-session coalescing, and the live zsh startup test.

  • tsc --noEmit -p config/tsconfig.node.json passes; oxlint and oxfmt --check are clean on the changed files.

  • Linux and Windows: behaviour unchanged. The new path runs only when the host is darwin (HOST_IS_DARWIN for the argv, process.platform === 'darwin' in the reader).

AI Disclosure

Written with Claude Code (Claude Opus 5.5) and reviewed and tested as described above.

Review

  • Cross-platform: macOS only, behind the existing HOST_IS_DARWIN argv guard and a darwin check in the reader. Linux keeps tty=; Windows never runs these argv.
  • SSH/remote/local: the naming runs wherever the capture runs (the local daemon or a relay host), against that same host's /dev.
  • Agents and integrations: provider-neutral; consumers see the same tty strings as before.
  • Performance: removes about 1.3 s of system time from every macOS capture on a busy host. The index costs one readdir plus an lstat per tty* entry on first use and when a new terminal appears.
  • Security: reads /dev entry metadata only; no new process execution or file writes.
  • UI: none.

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

Not addressed here, and possibly worth a follow-up: the snapshot TTL starts after capture completion, so on a host where the capture is slow it runs almost continuously. With this change a capture is short enough that this matters far less.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred): ran oxlint/oxfmt on the changed files, the node typecheck and the affected test directories locally; leaving the full lint, test and build to CI

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ewbhm4qQrr8ssHPL4gfo43

On macOS the full process-table capture asked `ps` for `tty=`, which names
each process's terminal with devname(3): a walk of /dev per process. On a
1,854-process Mac with 143 ptys that column alone cost 1.3s of system time,
and the whole capture 1.5-1.8 CPU-seconds, run nearly back to back by the
daemon.

Ask for `tdev=` (major/minor) instead and translate it back to the exact
name `tty=` prints from a /dev index of tty* and console entries, built once
and rebuilt when a capture names a terminal it has not seen. Consumers keep
receiving identical `tty` strings: on the measuring host the translated
capture matched `ps -o tty=` for all 1,969 processes, in 356ms against
6,559ms for the old argv under the same load.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ewbhm4qQrr8ssHPL4gfo43
@coderabbitai

coderabbitai Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

🧰 Additional context used
📚 Code guidelines (1)
AGENTS.md — auto-discovered
📝 Walkthrough

Walkthrough

On macOS, the process-table capture now requests tdev= instead of tty=. A new index maps terminal device IDs to names from /dev, and the reader applies those names before parsing the capture. Other platforms continue to use the raw capture. Tests cover device extraction, index lookup and caching, translation, and capture integration.

Priority: ➖ Normal

Severity of issue fixed: Medium

Merge Risk: 🔵 Low · up to c34f6

A reused terminal device number may continue to appear as a number rather than a terminal name. Address the cache behavior before merging, or accept this bounded macOS edge case.

Security Architecture Review

Security architecture risk: 🔵 Low · up to c34f6

The change stays within macOS process inspection, but terminal-name caching can retain failed lookups after recovery. Existing process-identity and process-group checks limit the impact; no security bypass was established.

Retained concerns

  • Low · reliability · observed: A transient per-entry lookup failure can become a long-lived cached miss. Subsequent fresh captures containing only previously missed devices do not retry naming them; recovery requires another device to trigger rebuilding or the module to restart. Raw device tokens then remain in terminal identity evidence instead of recovering the promised tty-name representation. This is a recovery and identity-contract concern, not an established authorization bypass.
Security review details

Security Blast Radius

  • inferred — The demonstrated exposure is terminal identity evidence derived from the existing macOS host-wide process capture, with downstream decisions scoped by PTY roots and process groups. The inspected change does not add authority to the capture command or establish a newly reachable external endpoint.

Trust Boundaries and Controls

  • observed — Remote evidence retains root anchoring, positive foreground-group and start-time requirements, descendant tty-boundary rejection, and multiplexer rejection. The shell-idle containment predicate separately counts numeric process groups and their members across the whole capture, without depending on terminal-name spelling.
  • inferred — Process command text is not interpolated into filesystem paths or executed by the new normalizer. Lookup paths come from filtered /dev directory entries, and the device-column matcher leaves command text outside its replacement. This limits a command-controlling local process's ability to redirect the added filesystem reads.

Resilience and Maintainability Implications

  • inferred — Raw device fallback generally preserves equality among rows on the same device within one capture, limiting the demonstrated ownership impact of failed naming. It does not establish exact compatibility with per-capture tty naming across recovery, alias changes, or device reuse.

Hardening Proposals

  • proposed — Bound negative-cache lifetime and distinguish transient stat failures from confirmed disappearance, allowing canonical terminal naming to recover without requiring unrelated device discovery.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 6 functions across 5 files. 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 and concisely describes the main change: using macOS tdev= values and translating them instead of requesting ps tty= names.
Description check ✅ Passed The description is complete and follows the repository template. It explains the change and rationale, links issue #24889, documents testing and platform impact, marks visual proof as not applicable, …
Linked Issues check ✅ Passed Issue #24889 requires a faster macOS full process-table capture while preserving the terminal names returned to consumers. PS_ARGS now uses tdev= only on macOS. nameDarwinTerminals maps device n…
Out of Scope Changes check ✅ Passed The changes stay within issue #24889. The new module implements macOS terminal-name translation. The new tests verify the implementation. The shell snapshot test updates its full-capture match to supp…
  • Fix all pre-merge checks with AI
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


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.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 6adc423a-4dce-4f7c-81af-634bdba5e998

📥 Commits

Reviewing files that changed from the base of the PR and between 75be95f and c34f637.

📒 Files selected for processing (5)
  • src/shared/darwin-terminal-names.test.ts
  • src/shared/darwin-terminal-names.ts
  • src/shared/process-table-snapshot-reader.ts
  • src/shared/process-table-snapshot.ts
  • src/shared/shell-foreground-snapshot.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

Comment on lines +75 to +94
const device = `${(stat.rdev >>> 24) & 0xff}/${stat.rdev & 0xffffff}`
if (!built.has(device)) {
built.set(device, name)
}
})
return built
}

return {
resolve: async (devices) => {
let current = names ?? (names = await build())
const missing = [...devices].filter((device) => !current.has(device) && !unnamed.has(device))
if (missing.length > 0) {
current = names = await build()
unnamed = new Set(missing.filter((device) => !current.has(device)))
}
return current
}
}
}

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '1,180p' src/shared/darwin-terminal-names.ts
sed -n '95,180p' src/shared/darwin-terminal-names.test.ts

Repository: stablyai/orca

Length of output: 7593


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- changed file and tests ---'
wc -l src/shared/darwin-terminal-names.ts src/shared/darwin-terminal-names.test.ts
sed -n '1,260p' src/shared/darwin-terminal-names.test.ts
printf '%s\n' '--- call sites and related contract text ---'
rg -n -C 3 'nameDarwinTerminals|createDarwinTerminalNameIndex|translateDarwinTerminalDevices|listDarwinTerminalDevices|darwin-terminal|tdev=|tty=' --glob '!node_modules' --glob '!dist' .
printf '%s\n' '--- PR diff summary and focused diff ---'
git diff --stat cc73c8e1a72b0e9ee9c29e57458ce307f5f019c2 c34f637ffa1ca9a90ced7d5c979aecdef563212d -- src/shared/darwin-terminal-names.ts src/shared/darwin-terminal-names.test.ts
git diff --unified=30 cc73c8e1a72b0e9ee9c29e57458ce307f5f019c2 c34f637ffa1ca9a90ced7d5c979aecdef563212d -- src/shared/darwin-terminal-names.ts src/shared/darwin-terminal-names.test.ts

Repository: stablyai/orca

Length of output: 41931


🤖 get_repo_knowledge executed:

get_repo_knowledge stablyai/orca /tmp/coderabbit-repo-knowledge/stablyai-orca-89dc44e4/conventions /tmp/coderabbit-repo-knowledge/stablyai-orca-89dc44e4/learnings

Length of output: 47480


🌐 Web query:

Darwin macOS pseudo terminal /dev/ttys device minor number reuse after close normal lifecycle dev_t allocation

💡 Result:

**Yes—PTY device-number reuse is a normal lifecycle behavior on Darwin.** A PTY opened through `/dev/ptmx` is a cloned device: the kernel assigns a minor number to the new PTY, and after the PTY is fully released that slot can be assigned to another PTY. So a saved `dev_t` is **not** a durable, unique identity for a PTY across close/reopen cycles.

One useful distinction: **closing one file descriptor does not necessarily free the PTY** if another descriptor or process still holds the master or slave open. Reuse is possible after the PTY itself is torn down, not simply after any individual `close()`.

Darwin’s `dev_t` encodes major and minor numbers; the XNU header’s `minor()` extracts the low 24 bits. That tells you how to inspect the number, but not how long a particular PTY minor remains assigned. ([github.com](https://github.com/apple/darwin-xnu/blob/main/bsd/sys/types.h?utm_source=openai))

**Caveat:** the available XNU source references are not all the same release, and the search results don’t establish an allocation-order or reuse guarantee for every macOS version. Treat the minor as an identifier for the *current* PTY instance, not as a persistent ID. ([newosxbook.com](https://www.newosxbook.com/src.php?file=%2Fbsd%2Fconf%2Ffiles&tree=xnu&utm_source=openai))

Citations:

- 1: https://github.com/apple/darwin-xnu/blob/main/bsd/sys/types.h?utm_source=openai
- 2: https://www.newosxbook.com/src.php?file=%2Fbsd%2Fconf%2Ffiles&tree=xnu&utm_source=openai

🏁 Script executed:

set -o pipefail
printf '%s\n' '--- process-table types and reader ---'
rg -n -C 5 'interface .*ProcessTable|type .*ProcessTable|tty:|terminal|createProcessTableCapture|nameDarwinTerminals|\.tty\b' src/shared src/main --glob '*.{ts,tsx}' | head -n 260
printf '%s\n' '--- focused reader ---'
sed -n '240,292p' src/shared/process-table-snapshot-reader.ts
printf '%s\n' '--- terminal matching consumers ---'
rg -n -C 6 'processTable|process table|ProcessTable|terminal.*tty|tty.*terminal|row\.tty|\.tty ===|\.tty !==|tty:' src/main src/shared --glob '*.{ts,tsx}' | head -n 320

Repository: stablyai/orca

Length of output: 41328


🌐 Web query:

site:github.com/apple/darwin-xnu pty minor reuse dev_t tty allocation

💡 Result:

The XNU search results **don’t establish the PTY minor-allocation or reuse behavior**. The closest relevant source is [`bsd/kern/tty.c`](https://github.com/apple/darwin-xnu/blob/main/bsd/kern/tty.c): `ttyopen()` records the supplied `dev_t`, and `ttyclose()` increments the TTY generation counter to help pending I/O detect that a TTY was recycled. That shows TTY-state recycling, but not how a PTY minor is allocated or reused. ([github.com](https://github.com/apple/darwin-xnu/blob/main/bsd/kern/tty.c?utm_source=openai))

XNU’s `dev_t` helpers encode the minor in the low 24 bits; that explains representation, not PTY allocation policy. ([github.com](https://github.com/apple/darwin-xnu/blob/main/bsd/sys/types.h?utm_source=openai))

**Unresolved:** whether closing a PTY makes its minor available for reuse, and what allocator selects the next minor. The search didn’t locate the PTY driver/allocation code needed to verify that.

Citations:

- 1: https://github.com/apple/darwin-xnu/blob/main/bsd/kern/tty.c?utm_source=openai
- 2: https://github.com/apple/darwin-xnu/blob/main/bsd/sys/types.h?utm_source=openai

🏁 Script executed:

set -o pipefail
printf '%s\n' '--- exact process-table files ---'
git ls-files '*process-table*'
rg -n -C 5 'tty' src/shared/process-table-snapshot.ts src/shared/process-table-snapshot-reader.ts src/main/daemon src/main/pty --glob '*.{ts,tsx}' || true
printf '%s\n' '--- all consumers of exported process-table readers/types ---'
rg -n -C 4 'readProcessTable|readProcessTableFresh|readStrictProcess|ProcessTableRow|StrictProcessTable|createProcessTableCapture|\.rows\b|\.processes\b' src --glob '*.{ts,tsx}' | head -n 300
printf '%s\n' '--- direct XNU source probes ---'
for p in bsd/kern/pty.c bsd/kern/tty.c bsd/sys/ttycom.h; do
  url="https://raw.githubusercontent.com/apple/darwin-xnu/main/$p"
  echo "### $p"
  curl -L --fail --silent --show-error "$url" | rg -n -C 3 'minor|dev_t|alloc|free|pty|clone|reuse|ttyclose' | head -n 120 || true
done

Repository: stablyai/orca

Length of output: 41870


🌐 Web query:

Darwin XNU pty.c pseudo terminal minor allocation reuse clone device source

💡 Result:

In current XNU, the PTY code is split across **`bsd/kern/tty_pty.c`** and **`bsd/kern/tty_dev.c`**—not a standalone `pty.c`.

- **Legacy PTYs:** `pty_init()` in `tty_pty.c` creates fixed `/dev/pty…` and `/dev/tty…` nodes for minor numbers `0` through `n_ptys - 1` (capped at `NPTY`). This is a fixed pool, not per-open minor allocation. ([github.com](https://github.com/apple-oss-distributions/xnu/blob/main/bsd/kern/tty_pty.c))
- **PTY state and reuse:** `tty_dev.c` dispatches by device major/minor to the registered driver. The legacy PTY driver’s `open` callback retrieves state from `pt_ioctl[minor]`; its `free` callback is optional. The `tty_dev_t` interface defines those callbacks. ([github.com](https://github.com/apple-oss-distributions/xnu/blob/main/bsd/kern/tty_dev.c))
- **Clone PTYs:** For dynamically cloned PTYs, follow the `/dev/ptmx` path in `tty_ptmx.c` and `tty_dev.c`; `tty_pty.c` describes the fixed-node implementation. The available source pages identify these files, but don’t establish the exact allocation/reuse algorithm for a particular XNU version. ([newosxbook.com](https://newosxbook.com/code/xnu-8792.81.2/bsd/kern/))

So, for **minor allocation/reuse**, start with `tty_ptmx.c` and the clone-device handling in `tty_dev.c`; for the **legacy fixed minor pool**, start with `pty_init()` in `tty_pty.c`. The linked source is current `main` where noted; no specific XNU release or commit was specified.

Citations:

- 1: https://github.com/apple-oss-distributions/xnu/blob/main/bsd/kern/tty_pty.c
- 2: https://github.com/apple-oss-distributions/xnu/blob/main/bsd/kern/tty_dev.c
- 3: https://newosxbook.com/code/xnu-8792.81.2/bsd/kern/

🏁 Script executed:

set -o pipefail
for p in bsd/kern/tty_pty.c bsd/kern/tty_ptmx.c bsd/kern/tty_dev.c; do
  url="https://raw.githubusercontent.com/apple-oss-distributions/xnu/main/$p"
  echo "### $p"
  curl -L --fail --silent --show-error "$url" |
    rg -n -C 5 'minor|ptmx|clone|alloc|free|open|close|pty_init|pt_ioctl|n_ptys|unit|slot' |
    head -n 240
done

Repository: stablyai/orca

Length of output: 18562


🏁 Script executed:

set -o pipefail
url='https://raw.githubusercontent.com/apple-oss-distributions/xnu/main/bsd/kern/tty_ptmx.c'
tmp=$(mktemp)
curl -L --fail --silent --show-error "$url" > "$tmp"
printf '%s\n' '--- ptmx allocation ---'
sed -n '269,390p' "$tmp"
printf '%s\n' '--- ptmx release ---'
rg -n -C 12 'ptmx_free_ioctl|pis_free|ptmx_clone' "$tmp"
rm -f "$tmp"

Repository: stablyai/orca

Length of output: 13118


🏁 Script executed:

curl -L --fail --silent --show-error \
  'https://raw.githubusercontent.com/apple-oss-distributions/xnu/main/bsd/kern/tty_ptmx.c' |
  sed -n '463,515p'

Repository: stablyai/orca

Length of output: 1575


Do not permanently cache unresolved Darwin device IDs.

When a PTY is released, Darwin can reuse its minor number for a new /dev/ttysXXX node. After the first lookup misses, unnamed prevents resolve from rebuilding the index for that number. Later captures can therefore keep the numeric major/minor value instead of the tty= name required by the process-table contract.

Suggested fix
   let names: Map<string, string> | null = null
-  let unnamed = new Set<string>()

   return {
     resolve: async (devices) => {
       let current = names ?? (names = await build())
-      const missing = [...devices].filter((device) => !current.has(device) && !unnamed.has(device))
+      const missing = [...devices].filter((device) => !current.has(device))
       if (missing.length > 0) {
         current = names = await build()
-        unnamed = new Set(missing.filter((device) => !current.has(device)))
       }
       return current
     }
   }

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

ℹ️ Minor suggestion inline — the /dev index's unnamed bookkeeping drifts from its documented intent.

Reviewed changes

  • macOS ps argv — PS_ARGS now requests tdev= instead of tty= on darwin (Linux unchanged), with a rationale comment.
  • Device→name translation — new src/shared/darwin-terminal-names.ts maps major/minor back to the /dev name tty= would print, behind a cached /dev index and a major/minor fallback.
  • Reader wiring — process-table-snapshot-reader.ts resolves names on darwin before parsing, best effort if /dev is unreadable.
  • Tests — new darwin-terminal-names.test.ts (translation, index walks, fallbacks) and an updated argv matcher in shell-foreground-snapshot.test.ts.

I verified the format against Apple's ps source (tdev prints "%d/%d", major(dev), minor(dev) and ?? for NODEV) and the Darwin dev_t layout, and confirmed every row.tty consumer only compares tty within a single snapshot, so the normal-case strings stay identical and the major/minor fallback preserves per-terminal grouping.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

const missing = [...devices].filter((device) => !current.has(device) && !unnamed.has(device))
if (missing.length > 0) {
current = names = await build()
unnamed = new Set(missing.filter((device) => !current.has(device)))

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.

unnamed is rebuilt from missing rather than from the devices the rebuild still could not name, so a device learned as unnameable is forgotten the moment a genuinely new terminal triggers a rebuild. The next capture naming the vanished device walks /dev again, which contradicts the "remembered so it does not cost a walk of /dev per capture" contract on lines 57-58 and 100. The test at line 120 never re-resolves 16/80 after the new terminal, so it cannot catch this.

Technical details
# `unnamed` loses previously-learned devices on rebuild

## Affected sites
- `src/shared/darwin-terminal-names.ts:87-90` — `unnamed = new Set(missing.filter(...))` drops any device not in this capture's `missing` set.
- `src/shared/darwin-terminal-names.ts:57-58`, `100` — comments promise an unnameable device "does not cost a walk of /dev per capture".
- `src/shared/darwin-terminal-names.test.ts:120-139` — stops before re-resolving the vanished `16/80` after `ttys081` appears.

## Required outcome
- A device a rebuild cannot name stays remembered across later rebuilds, so a live process holding a gone terminal node does not re-trigger a `/dev` walk every time a new terminal opens.
- Re-resolving the vanished device after a rebuild (the test at line 136) must not add a walk.

## Suggested approach (optional)
- Union instead of replace: `unnamed = new Set([...unnamed, ...missing].filter((device) => !current.has(device)))`.
- Secondary: on first use, any unnameable device makes `resolve` build twice back-to-back (`names ?? (names = await build())` then the `missing.length > 0` rebuild). The first build already reflects `/dev` now, so the immediate rebuild adds nothing; the rebuild should only run when `names` is a cached, potentially stale map.

@nwparker

Copy link
Copy Markdown
Contributor

Thanks @giorgio-zamparelli. This approach (naming macOS terminals from tdev= instead of ps tty=) landed in #27146, building on this PR, with a verified direct per-terminal lookup and an exact /dev fallback. You're credited as co-author on the merged commit. Closing as superseded.

@nwparker nwparker closed this Oct 10, 2026
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.

macOS: the daemon's full ps capture spends most of its time naming terminals (tty=), about a core on a busy Mac

2 participants