Skip to content

rmux_helper: add parent-pid-tree subcommand for tmux pane resolution#76

Merged
idvorkin merged 9 commits intoidvorkin:mainfrom
idvorkin-ai-tools:delegated/rmux-parent-pid-tree
Apr 14, 2026
Merged

rmux_helper: add parent-pid-tree subcommand for tmux pane resolution#76
idvorkin merged 9 commits intoidvorkin:mainfrom
idvorkin-ai-tools:delegated/rmux-parent-pid-tree

Conversation

@idvorkin-ai-tools
Copy link
Copy Markdown
Contributor

@idvorkin-ai-tools idvorkin-ai-tools commented Apr 14, 2026

Summary

  • Adds a parent-pid-tree subcommand to rmux_helper that walks the caller's parent-PID chain against tmux list-panes to deterministically find the caller's owning tmux pane.
  • Documents the new subcommand in rust/tmux_helper/CLAUDE.md with exit-code contract and usage.
  • Factors the walker into a DI-friendly helper with unit tests for the multi-session scenario, no-match case, and vanished-parent case.

Motivation

Observed 2026-04-14: harden-telegram's watchdog.py auto-recovery sent /reload-plugins to the wrong Claude session for ~45 minutes because it resolved the target pane via tmux display-message -p '#{pane_id}', which returns the tmux-active pane, not the caller's pane. With two Claude sessions running concurrently (Larry in pane %35, blog in pane %65), the watchdog reloaded the wrong session every time.

The correct primitive is a parent-PID walk: start from the caller's pid, read /proc/<pid>/stat field 4 for ppid, and match ancestors against tmux list-panes -a -F '#{pane_pid}'. First match is the answer. That's deterministic regardless of focus state.

This PR encapsulates that primitive in rmux_helper so future tmux-integration code can call rmux_helper parent-pid-tree and trust the answer instead of hand-rolling the walk. A companion PR in chop-conventions (#101) is fixing the specific watchdog.py case; this one makes the primitive reusable.

Test plan

  • cargo test passes with the new unit tests (181 total, 9 new)
  • cargo clippy --all-targets clean on new code (pre-existing link_picker warnings unchanged)
  • rmux_helper parent-pid-tree from inside a tmux pane prints that pane's id
  • rmux_helper parent-pid-tree --pid 1 (init) returns no-match, exit 1
  • rmux_helper parent-pid-tree --verbose shows the walk chain on stderr
  • rmux_helper parent-pid-tree --json emits valid JSON

Live smoke test output (from a tmux pane):

$ rmux_helper parent-pid-tree --verbose
parent-pid-tree: starting walk at pid 485370
parent-pid-tree: walked 485370 -> 4114505 -> 4114434 -> 2594534 (pane_pid) -> pane %35
%35

Follow-up: --tree

Added a --tree flag that prints the full ancestor chain as a visual tree with per-PID metadata: short comm, full cmdline (from /proc/<pid>/cmdline, null-separated argv joined with spaces), and exe readlink. Leverages the TmuxProvider / ProcReader DI layout from the earlier commits so the tree-builder and formatters are testable in isolation.

Composition:

  • --tree alone — ASCII tree on stdout, exit 0/1 matches the walk result
  • --tree --json — structured payload (start_pid, pane_id, pane_pid, chain[] with pid/comm/cmdline/exe)
  • --tree --verbose — tree on stdout, walk log on stderr (streams don't cross)
  • --tree --pid N — tree for an arbitrary start pid
  • Default (no --tree) — unchanged; still emits just %N

Kernel-thread / permission-denied paths degrade gracefully: empty cmdline falls back to [comm]; unreadable exe prints exe: (unreadable) (or null in JSON). Long cmdlines truncate at 120 chars with in the text view; full values always available via --json.

Eleven new tests cover: metadata collection, missing cmdline/exe handling, tree+json combination, no-match tree population (for debugging), formatter truncation, ASCII box-drawing, pane annotation at the leaf, and a real-/proc smoke test against the test process itself.

Live sample:

$ rmux_helper parent-pid-tree --tree
parent-pid-tree
├─ [pid 597447 ] zsh             /home/linuxbrew/.linuxbrew/bin/zsh -c source ...
│     exe: /home/linuxbrew/.linuxbrew/Cellar/zsh/5.9/bin/zsh
├─ [pid 4114505] claude          claude /startup-larry --dangerously-skip-permissions --channels plugin:telegram@claude-plugins-official
│     exe: /home/developer/.local/share/claude/versions/2.1.107
├─ [pid 4114434] larry_start.sh  /bin/bash ./larry_start.sh
│     exe: /usr/bin/bash
└─ [pid 2594534] zsh             /home/linuxbrew/.linuxbrew/bin/zsh  (pane shell)
      exe: /home/linuxbrew/.linuxbrew/Cellar/zsh/5.9/bin/zsh
      tmux pane: %35

🤖 Generated with Claude Code

Follow-up: shell completions

Adds an install-completions subcommand that writes shell completion scripts to the conventional location for zsh/bash/fish (powershell/elvish supported via --print-only). Auto-detects shell from $SHELL; override via --shell. Supports --dry-run (reports target path) and --print-only (stdout) for scripted use — mutually exclusive via clap.

rmux_helper install-completions              # auto-detect
rmux_helper install-completions --shell zsh  # explicit
rmux_helper install-completions --print-only # dump to stdout
rmux_helper install-completions --dry-run    # report path, skip write

Default install paths:

Shell Path
zsh $ZDOTDIR/.zfunc/_rmux_helper or $HOME/.zfunc/_rmux_helper
bash $XDG_DATA_HOME/bash-completion/completions/rmux_helper or $HOME/.local/share/bash-completion/completions/rmux_helper
fish $XDG_CONFIG_HOME/fish/completions/rmux_helper.fish or $HOME/.config/fish/completions/rmux_helper.fish

Dynamic completion — live values at tab-time

parent-pid-tree --pid <TAB> resolves live pids from /proc at tab-time, annotated with each process's comm as the completion description — capped at 500 entries, sorted newest-first so the most recently spawned processes appear first.

Uses clap_complete::CompleteEnv::with_factory(Cli::command).complete() at the top of main() (feature unstable-dynamic). When invoked with COMPLETE=<shell> in env, clap_complete intercepts and emits the shell script to stdout. The installed shell script re-invokes the binary with COMPLETE set on every tab press, so values are always live — no static snapshot to regenerate on upgrade.

Static completions (subcommand names, flag names, enum values like --shell <TAB>) come free from clap. side-edit <file> is annotated with ValueHint::FilePath for default shell file completion.

Live smoke

$ rmux_helper install-completions --shell zsh --dry-run
would install zsh completions to /home/developer/.zfunc/_rmux_helper

$ _CLAP_COMPLETE_INDEX=3 COMPLETE=zsh rmux_helper -- rmux_helper parent-pid-tree --pid "" | head -5
4114505:claude
4114434:larry_start.sh
4024124:tmux: client
4023867:bash
4023865:etterminal

Tests

14 new unit tests (shell detection, install-path resolution per shell+XDG, pid enumeration / truncation, missing-/proc tolerance, clap-level conflicts and unknown-shell rejection). cargo test now: 222 passed, 0 failed (was 208).

Summary by CodeRabbit

  • New Features

    • Added parent-pid-tree command with --json, --pid, and --verbose flags for resolving tmux pane ownership via parent process chain.
    • Added dynamic shell completion support for the new command.
  • Chores

    • Added dependency for shell completion generation.

Walks the caller's parent-PID chain from /proc/<pid>/stat against
`tmux list-panes -a -F '#{pane_id} #{pane_pid}'` to deterministically
find the owning tmux pane. Replaces `tmux display-message -p '#{pane_id}'`
for "which pane am I in" lookups — that primitive returns the focused
pane, not the caller's pane, and silently targets the wrong Claude
session when multiple sessions run concurrently.

Observed 2026-04-14: harden-telegram's watchdog.py reloaded the wrong
Claude session for ~45 minutes because it used display-message. The
companion chop-conventions PR (#101) fixes watchdog.py directly; this
PR makes the correct primitive callable from any tool that needs it.

Includes:
- ParentPidTree subcommand (--json, --pid, --verbose flags)
- DI-based resolve_pane_by_parent_chain helper with unit tests
- Docs in rust/tmux_helper/CLAUDE.md with exit-code contract

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 14, 2026

Warning

Rate limit exceeded

@idvorkin-ai-tools has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 54 minutes and 11 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 54 minutes and 11 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8656892e-49cf-4e62-bd5d-cfeb86de8a84

📥 Commits

Reviewing files that changed from the base of the PR and between bf7601e and c1f6f63.

📒 Files selected for processing (1)
  • rust/tmux_helper/src/main.rs
📝 Walkthrough

Walkthrough

Added comprehensive documentation for a new parent-pid-tree command that resolves tmux panes via parent-PID chain traversal, along with a dependency addition (clap_complete v4) to support shell completion generation. No implementation code is included in these changes.

Changes

Cohort / File(s) Summary
Documentation & Specification
rust/tmux_helper/CLAUDE.md
Added complete behavioral specification for parent-pid-tree command, including usage modes, flags (--json, --pid, --verbose), exit codes (0–3), implementation location reference, testable architecture components, and shell completion dynamics.
Dependency Management
rust/tmux_helper/Cargo.toml
Added clap_complete v4 dependency with unstable-dynamic feature to enable dynamic shell completion generation.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 Through parent PIDs we gently hop,
With tmux panes we'll never stop,
New docs chart the path so clear,
Completions bloom throughout the year! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main change: adding a parent-pid-tree subcommand to rmux_helper for resolving tmux panes via parent-PID chain walking.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

AI+idvorkin and others added 8 commits April 14, 2026 15:29
Addresses review feedback on PR idvorkin#76 — the original 9 tests covered
the core walker logic well but missed the sentinel guards and the
/proc parser. Adds 6 tests for:

- Walker starting from pid 0 and pid 1 (must return None without
  querying the reader; prior tests only matched pid 0/1 at the end
  of a chain, never at the start)
- read_ppid_from_proc pid 0 guard (early return, no fs hit)
- read_ppid_from_proc nonexistent pid (u32::MAX) — graceful None,
  not panic, exercises the .ok()? short-circuit
- read_ppid_from_proc against real /proc/1/stat on Linux — init's
  ppid is 0, which proves the rfind(')')-based comm parser works on
  actual kernel data (the exact bug the parser exists to prevent)
- read_ppid_from_proc(self) must return Some(non-zero) — end-to-end
  field-offset sanity check on a live stat line

No implementation changes; tests only. All 187 tests pass, clippy
clean on new code (4 pre-existing link_picker warnings unchanged).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PR idvorkin#76 review feedback asked for dependency injection at the CLI command
layer so surface tests for --json / --verbose / --pid / exit codes 2 and 3
become reachable. The previous round had to reject those tests because
parent_pid_tree_cmd called tmux and /proc directly and wrote to stdout via
println!, leaving no seam for a test to observe.

Humble Object pattern: TmuxProvider + ProcReader traits cover the shelling
layer; RealTmuxProvider and RealProcReader are the production impls. The
testable core run_parent_pid_tree takes the traits plus a ParentPidTreeArgs
struct and an explicit self_pid, and returns a ParentPidTreeOutcome
(stdout string, stderr lines, exit code). The thin parent_pid_tree_cmd
wrapper — the only code that constructs Real* impls and does real I/O —
is what main() still calls. No behavior change; 187 existing tests pass.

Addresses review feedback on PR idvorkin#76: add DI support to the CLI command
layer so CLI surface tests become reachable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous test-coverage pass had to reject CLI surface tests for --json
/ --verbose / --pid / exit codes 2 and 3 because there was no seam to
observe stdout/stderr or swap out tmux and /proc. With run_parent_pid_tree
now accepting injected TmuxProvider + ProcReader and returning a structured
ParentPidTreeOutcome, those tests become trivial.

Adds 11 new tests driving the testable core through in-memory mocks:
- default JSON output shape (pane_id, pane_pid, walked_from_pid, ancestors)
- --pid explicit override walks from that pid
- --pid explicit override does NOT consult self_pid's ppid (exit-3 bypass)
- default plain output is just the pane id (shell-assignable contract)
- --verbose emits walk chain to stderr with arrow format
- exit 1: no match, empty stdout, human-readable stderr
- exit 2: NotRunning variant, canonical stderr message
- exit 2: ListFailed variant carries io::Error detail in stderr
- exit 3: cannot read self ppid, empty stdout, path error on stderr
- flag combo: --json + --pid emits JSON from explicit pid
- flag combo: --json + --verbose keeps streams separated, no leakage

Brings the file-level test count from 187 to 198. No behavior change.

Addresses review feedback on PR idvorkin#76: add CLI surface tests previously
rejected as unreachable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The other tmux call sites in this binary (side_edit, side_run, rename_all,
rotate, third, info, etc.) currently shell out directly via run_tmux_command
and scattered Command::new("tmux") calls. None of them have characterization
tests, so bulk-migrating them to the new TmuxProvider trait would lose the
"tests pass" safety net. Igor asked for the trait "throughout" — doing that
safely requires tests first.

Leaves a TODO comment above run_tmux_command flagging the unmigrated sites,
and documents the humble-object pattern in tmux_helper/CLAUDE.md so future
additions follow it: put shell-outs behind TmuxProvider, keep logic in a
pure function that takes the trait object, make the command wrapper thin.

Addresses review feedback on PR idvorkin#76: scope management — migrate where
safe, flag the rest as follow-up work.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tails

Prints the full ancestor walk as a human-readable tree with
cmdline / comm / exe per PID, plus the terminal pane_id. Uses the
existing TmuxProvider / ProcReader traits (from 47b1ebb) so the
tree-building logic and the formatter are both testable via DI.

`--tree` composes cleanly with `--json` (structured chain output),
`--verbose` (walk lines on stderr, tree on stdout), and `--pid`
(override start pid). Default behavior unchanged when the flag is
omitted.

Example:
$ rmux_helper parent-pid-tree --tree
parent-pid-tree
├─ [pid 583169]  bash    /usr/bin/bash -c rmux_helper parent-pid-tree --tree
│     exe: /usr/bin/bash
├─ [pid 4114505] claude  claude /startup-larry ... --channels plugin:telegram@claude-plugins-official
│     exe: /home/developer/.local/bin/claude
└─ [pid 2594534] zsh     /home/linuxbrew/.linuxbrew/bin/zsh  (pane shell)
      exe: /home/linuxbrew/.linuxbrew/bin/zsh
      tmux pane: %35

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a new install-completions subcommand that generates shell
completions (zsh/bash/fish/powershell/elvish) and writes them to
conventional paths. Supports --print-only (stdout) and --dry-run
(report target path only). Powered by clap_complete with dynamic
completion enabled — `parent-pid-tree --pid <TAB>` resolves live
pids from /proc at tab-time, annotated with each process's comm.

Humble-object refactor: detect_shell_from_env, completion_install_path,
and enumerate_pid_candidates are pure functions driven by an
EnvSnapshot struct + injected ProcReader, covered by 14 new unit
tests.

Recovered from an in-progress working tree left by a sibling subagent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Companion doc update for the install-completions subcommand. Covers
default install paths, --print-only / --dry-run semantics, the tab-time
dynamic completion hook, and the clap_complete unstable-dynamic feature
flag dependency.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… tree

Previously, rich per-pid metadata (cmdline/comm/exe) only appeared in
JSON when --tree was set. --json --verbose emitted the minimal payload
plus stderr walk lines — useful but forced callers to pick between
human-readable tree OR structured rich JSON. Now either --tree or
--verbose triggers the full chain[] array in JSON, with --verbose
also keeping the stderr walk lines for human inspection. The minimal
--json output (without either detail flag) stays untouched for
scriptability.

Also adds role markers to distinguish walk positions:
  - Tree text: "(start)" on the root line, "(pane shell)" on the
    leaf (already existed), "(no pane found)" when exit 1, and
    "(start, pane shell)" / "(start, no pane found)" for
    single-entry chains.
  - JSON: `role` field per entry with values {start, ancestor,
    pane_shell, start_and_pane_shell, walked_past_root}.

Role derivation is extracted into a pure `chain_entry_role` helper so
tests cover the policy independently of the walker. Args struct gains
`wants_rich_chain()` predicate to keep the rich/minimal JSON switch
in one place.

Observed by Igor in interactive use: the old output made root and
leaf visually identical mid-chain, hiding which pid the walk
actually started from when passing --pid <N>.

Adds 12 new tests (234 total, was 222): rich vs minimal JSON paths,
verbose text-mode unchanged, tree-text start annotation, single-entry
combined annotation, no-pane-found leaf annotation, JSON role values
for each walk shape, and a pure unit test for chain_entry_role.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@idvorkin idvorkin merged commit 537762a into idvorkin:main Apr 14, 2026
1 check passed
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.

2 participants