This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

feat: set process.title so pty processes are identifiable in ps/top/htop (#48)

Under Node 24+, V8 names the main thread "MainThread", so every pty
process (the bin/pty signal-forwarding wrapper, the CLI, each session
daemon, and the supervisor) shows up as `MainThread` in ps/top/htop/btm.
That makes them indistinguishable from any other Node process and hurts
observability and leak triage.

Set `process.title` early in each entry point so they report a
meaningful name instead:

- bin/pty -> "pty" (signal-forwarding wrapper)
- src/cli.ts -> "pty" (CLI)
- src/server.ts -> "pty-daemon" (session daemon, guard-scoped)
- src/supervisor-entry.ts-> "pty-supervisor" (supervisor)

`process.title` is the only thing that overrides /proc/<pid>/comm, and
only when set from within the running process after V8 init — launch
flags (`node --title`, `exec -a`) do not work. Linux caps comm at 15
chars (TASK_COMM_LEN); all titles stay well under. Each assignment is
wrapped in try/catch so it can never throw. server.ts only sets the
title inside its `argv[1].endsWith("/server.js")` daemon-entry guard,
since the module is also imported as a library (PtyServer).

Adds a Linux-only test asserting a spawned daemon's /proc/<pid>/comm is
"pty-daemon".


Claude-Session: https://claude.ai/code/session_01FKmH5V4zLM7HXsbNohqPV9

Co-authored-by: schickling-assistant <schickling.ventures@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

authored by

AI assistant of @schickling
schickling-assistant
Claude Opus 4.8
and committed by
GitHub
(Jul 2, 2026, 1:03 PM +0200) b88d9fe7 48d954a0

+88
+7
bin/pty
··· 1 1 #!/usr/bin/env node 2 + // Give this signal-forwarding wrapper a meaningful process name. Under Node 24+ 3 + // V8 names the main thread "MainThread", so without this every pty process shows 4 + // up as `MainThread` in ps/top/htop/btm. `process.title` is the only thing that 5 + // overrides /proc/<pid>/comm, and only when set from within the running process 6 + // after V8 init. Linux caps comm at 15 chars (TASK_COMM_LEN), so keep it short. 7 + try { process.title = 'pty'; } catch {} 8 + 2 9 import { existsSync } from 'node:fs'; 3 10 import { spawn } from 'node:child_process'; 4 11 import { fileURLToPath } from 'node:url';
+9
src/cli.ts
··· 38 38 import { extractFilterTags as extractFilterTagsImpl, matchesAllTags, isReservedTagKey } from "./tags.ts"; 39 39 import { parseDuration, formatDuration } from "./duration.ts"; 40 40 41 + // Name this process so it shows up meaningfully in ps/top/htop/btm instead of 42 + // "MainThread" (V8's default main-thread name under Node 24+). `process.title` 43 + // is the only thing that overrides /proc/<pid>/comm, and only when set from 44 + // within the running process after V8 init — launch flags like `node --title` 45 + // or `exec -a` do not work. Linux truncates comm at 15 chars (TASK_COMM_LEN). 46 + // This module is only ever an entrypoint (it calls main() on load), so setting 47 + // the title at module scope is safe. 48 + try { process.title = "pty"; } catch {} 49 + 41 50 // Lazy-load the interactive TUI so non-interactive commands don't crash when 42 51 // the caller's cwd was deleted (the TUI module evaluates process.cwd() at load). 43 52 async function runInteractive(options?: { preselectNew?: boolean; filterTags?: Record<string, string>; force?: boolean }): Promise<void> {
+8
src/server.ts
··· 966 966 967 967 /** Entry point when this file is run as the daemon process. */ 968 968 if (process.argv[1]?.endsWith("/server.js")) { 969 + // Name the daemon process so it shows up as "pty-daemon" in ps/top/htop/btm 970 + // rather than "MainThread" (V8's default main-thread name under Node 24+). 971 + // This is set only inside the daemon-entry guard — server.ts is also imported 972 + // as a library (PtyServer), and we must not rename those host processes. 973 + // `process.title` is the only override for /proc/<pid>/comm and Linux caps it 974 + // at 15 chars (TASK_COMM_LEN), so "pty-daemon" (10 chars) stays well under. 975 + try { process.title = "pty-daemon"; } catch {} 976 + 969 977 const config = JSON.parse(process.env.PTY_SERVER_CONFIG ?? "{}"); 970 978 if (!config.name || !config.command) { 971 979 console.error("PTY_SERVER_CONFIG env var required");
+64
tests/process-title.test.ts
··· 1 + import { describe, it, expect, afterEach, afterAll } from "vitest"; 2 + import * as fs from "node:fs"; 3 + import * as os from "node:os"; 4 + import * as path from "node:path"; 5 + import { fileURLToPath } from "node:url"; 6 + import { spawnSync } from "node:child_process"; 7 + 8 + // The daemon sets `process.title = "pty-daemon"` so it is identifiable in 9 + // ps/top/htop/btm instead of showing V8's default main-thread name 10 + // ("MainThread" under Node 24+). The only OS-visible proof is 11 + // /proc/<pid>/comm, which exists on Linux only — so the comm assertion is 12 + // gated on platform. Linux caps comm at 15 chars (TASK_COMM_LEN); the title 13 + // values used here are all well under that. 14 + 15 + const __dirname = path.dirname(fileURLToPath(import.meta.url)); 16 + const nodeBin = process.execPath; 17 + const cliPath = path.join(__dirname, "..", "dist", "cli.js"); 18 + const isLinux = process.platform === "linux"; 19 + 20 + const testRoot = fs.mkdtempSync(path.join(os.tmpdir(), "pty-title-")); 21 + afterAll(() => { 22 + fs.rmSync(testRoot, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }); 23 + }); 24 + 25 + let bgPids: number[] = []; 26 + 27 + function readComm(pid: number): string { 28 + return fs.readFileSync(`/proc/${pid}/comm`, "utf-8").trim(); 29 + } 30 + 31 + afterEach(() => { 32 + for (const pid of bgPids) { 33 + try { process.kill(pid, "SIGTERM"); } catch {} 34 + } 35 + bgPids = []; 36 + }); 37 + 38 + describe("daemon process title", () => { 39 + it.skipIf(!isLinux)("daemon process comm is 'pty-daemon'", () => { 40 + const sessionDir = fs.mkdtempSync(path.join(testRoot, "sd-")); 41 + const name = "title-test"; 42 + const r = spawnSync( 43 + nodeBin, 44 + [cliPath, "run", "-d", "--id", name, "--no-display-name", "--", "sleep", "30"], 45 + { 46 + cwd: os.tmpdir(), 47 + env: { ...process.env, PTY_SESSION_DIR: sessionDir }, 48 + encoding: "utf-8", 49 + timeout: 15000, 50 + }, 51 + ); 52 + expect(r.status, r.stderr).toBe(0); 53 + 54 + const pid = parseInt( 55 + fs.readFileSync(path.join(sessionDir, `${name}.pid`), "utf-8").trim(), 56 + 10, 57 + ); 58 + expect(Number.isInteger(pid)).toBe(true); 59 + bgPids.push(pid); 60 + 61 + // comm is capped at 15 chars; "pty-daemon" (10) is not truncated. 62 + expect(readComm(pid)).toBe("pty-daemon"); 63 + }); 64 + });