Repository navigation
Retire stale sidebar port badges despite vanished TTYs, privileged owners, and zombies - #9324
Conversation
Sidebar port badges accumulate dead ports for the lifetime of the app because three independent mechanisms each leave a panel's scan permanently incomplete, and PortScanSnapshotReconciler treats incomplete scans as non-evidence that only ever unions ports: - BSD `ps` aborts an entire batched `-t` query when any listed terminal device is gone, so one closed pty makes every panel's port scan look incomplete. - `proc_pidinfo` refuses any process whose effective UID differs from ours, so the root-owned `/usr/bin/login` heading every terminal reads as unidentifiable, files under `incompletePIDs`, and scores its panel incomplete on every scan. - A zombie answers `kill(pid, 0)` like a running process while holding no readable identity — the same incompleteness, reachable through any unreaped child, though a zombie has exited and can hold no socket. An end-to-end test also drives the real scanner — registerTTY, kick, coalesce, burst, reconcile, publish — and asserts a port is retired once its process stops listening, since these bugs left every stage passing its own unit test while the feature was fully broken. Identity and presence deliberately stay on the real providers: an earlier draft injected them and passed against the broken tree. Covers github.com/manaflow-ai/issues/9152. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016pmrs1n9Z2UKquWcbXhmBe
One fix per mechanism the failing tests pin: - `runPS` drops the terminals `ps` reports as ENOENT and retries with the rest, bounded so a pty churning mid-scan cannot spin. Terminals that are all gone report authoritative emptiness — a freed pty can hold no process — so their stale badges clear too, while any other diagnostic still yields incomplete, which retains ports rather than dropping them on weak evidence. Vanished terminals are matched by device name, so the two-device diagnostic form (`/dev/ttyX and /dev/X`) and TTYs registered by full device path are recognized, and "every terminal is gone" outranks the retry budget so authoritative emptiness does not depend on which attempt the final pty closed during. - Birth identities are read through `sysctl(KERN_PROC_PID)` instead of `proc_pidinfo`. It reports the same birth timestamp for any live process regardless of owner, and still reports nothing for an exited PID, so recycling detection keeps working and now covers privileged PIDs it previously had to guess about. `SZOMB` is rejected explicitly: sysctl also describes an exited-but-unreaped process, and session restore treats a matching identity as proof the agent is alive. - `PIDPresence` routes through the same process-table read that supplies birth identities, so liveness and identity cannot drift apart, and an unreaped process reads as absent to every caller weighing whether it might still own something. `Workspace.agentPIDProcessIdentity(pid:)` reads through that single reader as well instead of keeping its own `proc_pidinfo` copy. The tests added here pin behavior this fix introduces rather than the original bug: zombie identities reading as absent, diagnostic forms that must not trigger the retry, and the retry budget staying incomplete when a TTY never becomes scannable. Fixes manaflow-ai#9152. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016pmrs1n9Z2UKquWcbXhmBe
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe change uses ChangesProcess identity and zombie handling
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant PortScanner
participant ps
participant ProcessIdentity
participant PanelSnapshotReconciler
PortScanner->>ps: scan normalized TTYs
ps-->>PortScanner: process output and diagnostics
PortScanner->>ProcessIdentity: validate process identity
ProcessIdentity-->>PortScanner: valid identity or absent process
PortScanner->>PanelSnapshotReconciler: publish complete or incomplete scan
PanelSnapshotReconciler-->>PortScanner: retain or retire missing ports
Possibly related issues
Possibly related PRs
Suggested reviewers: Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (2 errors, 1 warning)
✅ Passed checks (22 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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 |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 `@Sources/PortScanner`+Process.swift:
- Around line 374-397: Update the ps invocation used by runPS and commandRunner
so its child environment explicitly sets a deterministic message locale (LC_ALL
or LC_MESSAGES) before executing /bin/ps, following the runner’s existing
environment-construction pattern. Add a focused test asserting the command
runner receives that locale override, while preserving existing environment
values and vanishedTTYNames parsing behavior.
🪄 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: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: d72f3035-1a14-4cff-8099-54879c6f3c5e
📒 Files selected for processing (5)
Sources/AgentPIDProcessIdentity.swiftSources/PIDPresence.swiftSources/PortScanner+Process.swiftSources/Workspace+PanelLifecycle.swiftcmuxTests/PortScannerTests.swift
There was a problem hiding this comment.
All reported issues were addressed across 5 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
Review asked to pin LC_ALL on the ps invocation because vanishedTTYNames matches the English strerror(ENOENT) text. Darwin libc ships no localized message catalogs, so ps emits this exact string under any locale (verified empirically with LC_ALL=ja_JP.UTF-8); record that constraint instead of widening the CommandRunning API for an unreachable failure mode. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PDYSKWqKSHrr6MAuMtou9C
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
Sources/PortScanner+Process.swift (1)
340-352: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winReject mixed or failed
psresults before returning complete emptiness.
runPSreturns.completewhenvanishedTTYNamesremoves every remaining TTY. This branch does not verifyparsedEveryRow,result.executionError,result.timedOut, or whetherstderrcontains unrecognized diagnostics. A result with a recognizedENOENTline plus another error, malformed output, or a timeout can therefore retire ports as if the scan were authoritative. This contradicts the comment on Lines 372-373.Make the parser report whether all diagnostics were recognized. Require a healthy, fully parsed command result before returning complete emptiness. Add regression coverage for mixed diagnostics and failed or malformed results.
Also applies to: 367-401
🤖 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 `@Sources/PortScanner`+Process.swift around lines 340 - 352, Update the runPS result parsing around vanishedTTYNames to report whether every stderr diagnostic was recognized, while preserving malformed-row detection via parsedEveryRow. Before returning .complete for empty remaining TTYs, require a healthy execution with no executionError, no timeout, fully parsed rows, and no unrecognized diagnostics; otherwise continue returning .incomplete. Add regression coverage for mixed diagnostics, failed or timed-out commands, and malformed output.
🤖 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.
Outside diff comments:
In `@Sources/PortScanner`+Process.swift:
- Around line 340-352: Update the runPS result parsing around vanishedTTYNames
to report whether every stderr diagnostic was recognized, while preserving
malformed-row detection via parsedEveryRow. Before returning .complete for empty
remaining TTYs, require a healthy execution with no executionError, no timeout,
fully parsed rows, and no unrecognized diagnostics; otherwise continue returning
.incomplete. Add regression coverage for mixed diagnostics, failed or timed-out
commands, and malformed output.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: ae4fadf1-ac83-4245-8db2-d5635f6ecc0a
📒 Files selected for processing (1)
Sources/PortScanner+Process.swift
Review claimed a vanished TTY could launder another terminal's failed diagnostic into a complete scan. It cannot: non-ENOENT diagnostics never remove a terminal from the retry set, so completeness requires either a clean re-query or explicit ENOENT for every terminal. Pin the exact mixed case: ENOENT for one TTY plus Permission denied for another re-queries only the unreadable terminal and stays incomplete when its diagnostic persists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PDYSKWqKSHrr6MAuMtou9C
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
cmuxTests/PortScannerTests.swift (3)
865-885: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMake unexpected scripted invocations fail with an explicit error.
ScriptedCommandRunnerrepeats its final result after the script is exhausted. An extrapsorlsofretry can therefore look valid. Make exhaustion fail by default, or add an explicit repeat mode only for tests that need steady-state polling.🤖 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 `@cmuxTests/PortScannerTests.swift` around lines 865 - 885, The ScriptedCommandRunner.run method currently reuses the final scripted result after exhaustion, allowing unexpected invocations to pass. Change it to fail explicitly when recordedArguments exceeds results, or add an opt-in repeat mode while keeping strict exhaustion as the default; preserve repeating behavior only for tests that explicitly request it.
111-121: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winMake the root-owned identity test prove the fallback path.
The test only checks that
AgentPIDProcessIdentity(pid: 1)returns a value. It does not verify thatproc_pidinfois unavailable. If the test target runs as root, the old lookup can also pass this test. Run this case as an unprivileged user and assert the old lookup is denied, or obtain a process owned by another user before testing thesysctlfallback.🤖 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 `@cmuxTests/PortScannerTests.swift` around lines 111 - 121, Update otherUsersProcessHasReadableIdentity to explicitly verify the unprivileged fallback scenario: ensure the test runs without root privileges and assert the direct proc_pidinfo lookup for the selected root-owned process is denied before validating AgentPIDProcessIdentity(pid: 1). If root execution cannot be controlled, select a process owned by another user and assert the old lookup fails while the sysctl-backed identity succeeds.
120-145: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winImport Darwin for the POSIX Darwin symbols.
cmuxTests/PortScannerTests.swiftusesgetpid,posix_spawn,waitpid,environ,usleep,sysctl,kinfo_proc,SZOMB, andpid_t, but only imports framework modules. Addimport Darwinso these tests compile without depending on an accidental re-export.🤖 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 `@cmuxTests/PortScannerTests.swift` around lines 120 - 145, Add import Darwin to PortScannerTests.swift so the POSIX and Darwin symbols used by zombieProcessHasNoReadableIdentity and the surrounding process identity tests—such as getpid, posix_spawn, waitpid, environ, usleep, sysctl, kinfo_proc, SZOMB, and pid_t—are explicitly available.
🤖 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.
Outside diff comments:
In `@cmuxTests/PortScannerTests.swift`:
- Around line 865-885: The ScriptedCommandRunner.run method currently reuses the
final scripted result after exhaustion, allowing unexpected invocations to pass.
Change it to fail explicitly when recordedArguments exceeds results, or add an
opt-in repeat mode while keeping strict exhaustion as the default; preserve
repeating behavior only for tests that explicitly request it.
- Around line 111-121: Update otherUsersProcessHasReadableIdentity to explicitly
verify the unprivileged fallback scenario: ensure the test runs without root
privileges and assert the direct proc_pidinfo lookup for the selected root-owned
process is denied before validating AgentPIDProcessIdentity(pid: 1). If root
execution cannot be controlled, select a process owned by another user and
assert the old lookup fails while the sysctl-backed identity succeeds.
- Around line 120-145: Add import Darwin to PortScannerTests.swift so the POSIX
and Darwin symbols used by zombieProcessHasNoReadableIdentity and the
surrounding process identity tests—such as getpid, posix_spawn, waitpid,
environ, usleep, sysctl, kinfo_proc, SZOMB, and pid_t—are explicitly available.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 01ecb718-3f45-49a9-8082-8bd566862c4f
📒 Files selected for processing (1)
cmuxTests/PortScannerTests.swift
|
@coderabbitai Regarding the outside-diff finding on
The comment on lines 372-373 describes exactly this: only ENOENT is treated as absence; every other diagnostic leaves the scan incomplete. No change made. |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
fd -a -t f 'PortScanner\+Process\.swift|CommandRunner\.swift|PortScannerTests\.swift' . \
| sort \
| while IFS= read -r f; do
echo "===== OUTLINE: $f ====="
ast-grep outline "$f" --items all --view compact || true
done
echo "===== runPS and nearby logic ====="
rg -n -C 18 '\brunPS\b|vanished|parsedEveryRow|remaining|ENOENT|\.complete|\.incomplete' Sources cmuxTests \
-g 'PortScanner+Process.swift' -g 'CommandRunner.swift' -g 'PortScannerTests.swift'
echo "===== cited regression test ====="
rg -n -A 70 -B 12 '\bmixedVanishedAndUnreadableDiagnosticsStayIncomplete\b' cmuxTests -g 'PortScannerTests.swift'Length of output: 103424
A non-ENOENT diagnostic does not remove its TTY from The all-vanished result is authoritative negative evidence for the queried TTY set. 🐇 ✏️ Learnings added
You are interacting with an AI system. |
Issue manaflow-ai#9152 reports a Time Machine filesystem warning on every lsof invocation. Model that warning in the full port lifecycle harness and prove PID-scoped TCP evidence must remain authoritative, while tightening the existing process-identity and retry test seams. This is the tests-only red half of the regression pair.
Pass -w to the PID-scoped TCP listener query so unrelated filesystem mount warnings cannot poison every scan. Actual command failures, timeouts, malformed output, and PID-scoped uncertainty remain incomplete. This is the green half for the persistent-warning regression in 6b54e60.
A single kick near the end of an existing scan burst can receive fewer than the three authoritative misses required to retire a stale port. Pin that idle-workspace timing path before changing the scheduler.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
Sources/PortScanner+Process.swift (1)
334-334: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winNormalize live TTY identities before joining scan results.
A panel can register
/dev/ttys001, butps -o tty=returnsttys001. Line 334 preserves the latter form. Later exact comparisons against the registered TTY then find no panel PIDs or ports.The new test at
cmuxTests/PortScannerTests.swiftLines 476-505 covers only the vanished-device case. Add coverage for a live full-path TTY. Use one canonical device name when buildingpidsByTTY, calculating panel completeness, and joiningportsByTTY.As per path instructions, pane identity must use one reliable structured source instead of disagreeing representations.
<path_instructions>🤖 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 `@Sources/PortScanner`+Process.swift at line 334, Normalize the TTY value assigned in the process-mapping logic at mapping[pid] so live ps values such as ttys001 use the same canonical device representation as registered panel TTYs, such as /dev/ttys001. Apply that canonical name consistently when building pidsByTTY, calculating panel completeness, and joining portsByTTY. Extend PortScannerTests with coverage for a live full-path TTY, not only a vanished device.Source: Path instructions
🤖 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 `@cmuxTests/PortScannerTests.swift`:
- Line 1041: Update waitForLsofInvocation and its call sites around lines 1041
and 1119-1124 so waiting for lsofInvocationCount is deadline-bounded and fails
when the target count is not reached. Replace the indefinitely suspended
continuation-dictionary approach with a bounded poll of the real
invocation-count predicate, or add equivalent cancellation and timeout handling
to every waiter, and require the bounded wait to complete successfully at the
affected test points.
---
Outside diff comments:
In `@Sources/PortScanner`+Process.swift:
- Line 334: Normalize the TTY value assigned in the process-mapping logic at
mapping[pid] so live ps values such as ttys001 use the same canonical device
representation as registered panel TTYs, such as /dev/ttys001. Apply that
canonical name consistently when building pidsByTTY, calculating panel
completeness, and joining portsByTTY. Extend PortScannerTests with coverage for
a live full-path TTY, not only a vanished device.
🪄 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: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: a2c5c020-9624-4c18-aea3-4619108fb663
📒 Files selected for processing (3)
Sources/PortScanner+Process.swiftSources/PortScanner.swiftcmuxTests/PortScannerTests.swift
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 `@cmuxTests/AgentHibernationTests.swift`:
- Around line 948-960: Update Workspace.applySessionPanelMetadata so snapshots
with terminal?.hibernation != nil remove the target panel from
surfaceListeningPorts instead of restoring snapshot.listeningPorts. Preserve the
existing persisted-port restoration behavior for non-hibernated panels so
recomputeListeningPorts excludes stale ports and badges after restore.
🪄 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: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 3bede7d6-f8ef-4062-b8a0-06fee7d5858a
📒 Files selected for processing (4)
Sources/PortScanner+Process.swiftSources/PortScanner.swiftcmuxTests/AgentHibernationTests.swiftcmuxTests/PortScannerTests.swift
Adversarial maintainer verificationVerdict: fixes #9152. I traced the full local path (
Additional gaps found and fixedI appended four test-first/fix pairs, preserving the contributor's approach:
The corrected hibernation red run is important: the earlier method-filtered runs selected zero Swift Testing tests and were not valid evidence. The class-level test-only run executed 36 tests and failed only the two new regressions (six concrete expectations); the final-head run executed the same 36 and passed. Safety, race, and performance checks
I did not take the automated suggestion to extract a small Red/green and final validation
The explicit macOS-26 I then bypassed that unrelated app-host path with an isolated SwiftPM executable harness on this macOS 26.4.1 host. The harness compiled the branch exact
Verdict: fixes #9152. |
Fixes #9152: sidebar port badges accumulated dead ports for the lifetime of the app — a port, once discovered, could never retire.
Root causes
Three independent mechanisms each left a panel's scan permanently incomplete, and
PortScanSnapshotReconcilertreats incomplete scans as non-evidence that only ever unions ports:psaborts an entire batched-tquery when any listed terminal device is gone, so a single closed panel made every panel's scan look incomplete.proc_pidinforefuses any process whose effective UID differs from ours, so the root-owned/usr/bin/loginheading every terminal filed underincompletePIDsand scored its panel incomplete on every scan.kill(pid, 0)like a running process while holding no readable identity — the same incompleteness, reachable through any unreaped child.Fixes
runPSdrops terminalspsreports as ENOENT and retries with the rest (bounded). All terminals gone reports authoritative emptiness — a freed pty can hold no process — so closed panels' stale badges clear too. Any other diagnostic still yields incomplete, retaining ports rather than dropping them on weak evidence.sysctl(KERN_PROC_PID)instead ofproc_pidinfo: same birth timestamp regardless of owner, still nothing for an exited PID,SZOMBrejected explicitly so session restore does not treat a dead agent as alive.PIDPresenceroutes through the same process-table read that supplies identities, so liveness and identity cannot drift apart; a zombie reads as absent to every caller weighing whether it might still own a socket.Commit structure
Two commits per the regression-test policy: commit 1 adds nine failing tests (CI red) that reproduce all three mechanisms against
mainplus an end-to-end test driving the real scanner (registerTTY → kick → coalesce → burst → reconcile → publish); commit 2 adds the fix (CI green) with four additional tests pinning behavior the fix introduces (zombie identity rejection, diagnostic forms that must not trigger the retry, retry-budget exhaustion).Verification
mainbase (9 expected failures at commit 1, 27/27 pass at commit 2).AgentSessionAutoResumeSwiftTests,SessionPersistenceResumeBindingTests,ClaudeHookPIDAuthenticationTests) fail identically on this branch andmainin the local environment (pre-existing/environmental only).Notes
ps/lsofsubprocesses with libproc #3801 (libproc rewrite of the scanner) would eventually subsume theps -tretry; this PR fixes the shipped behavior without waiting on that rewrite.🤖 Generated with Claude Code
https://claude.ai/code/session_016pmrs1n9Z2UKquWcbXhmBe
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.Summary by cubic
Retires stale sidebar port badges so ports clear when listeners stop and when a terminal panel hibernates. Fixes #9152.
sysctl(KERN_PROC_PID); rejects zombies;PIDPresenceandWorkspace.agentPIDProcessIdentityshare the same reader.lsof: adds-wto suppress unrelated filesystem warnings.lsofwarnings, late-burst kicks, zombies, full-path TTY attribution, and hibernation/restore port retirement.Written for commit e7c4253. Summary will update on new commits.
Summary by CodeRabbit
Bug Fixes
Tests