This repository has no description
0

Configure Feed

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

fix(list): preserve sessions whose daemon is alive but socket is unreachable

listSessions() previously deleted .sock files whenever a socket was
unreachable, even if the recorded pid was still alive. That made any
transient socket-reachable failure (busy daemon, EAGAIN, race with a
service restart) permanent: once the .sock is removed, the still-alive
daemon becomes invisible to all future scans.

It also called cleanupAll() on .json files older than 24h without
checking whether the daemon process was still running, so long-lived
daemons silently lost their metadata after a day and disappeared from
'pty list' even though they kept consuming RAM.

This commit makes both checks gated on isProcessAlive(pid):
- if pid is alive but socket is unreachable, keep both .sock and metadata,
report status as running
- if .json is older than 24h, only cleanupAll() when pid is dead

Refs: https://github.com/myobie/pty/issues/34

schickling-assistant (May 3, 2026, 4:28 AM +0200) 46c03444 0dc1c8f5

+69 -4
+17 -4
src/sessions.ts
··· 361 361 // reachable socket can briefly coexist with exitedAt being set. 362 362 const status = metadata?.exitedAt ? "exited" : "running"; 363 363 sessions.push({ name, socketPath, pid, status, metadata }); 364 + } else if (pid !== null && isProcessAlive(pid)) { 365 + // Pid is still alive but the socket isn't reachable right now (busy 366 + // daemon, transient EAGAIN, race with a service restart). Keep the 367 + // socket on disk and report the session as running — deleting the 368 + // socket would render the still-alive daemon permanently invisible. 369 + const metadata = readMetadata(name); 370 + const status = metadata?.exitedAt ? "exited" : "running"; 371 + sessions.push({ name, socketPath, pid, status, metadata }); 364 372 } else { 365 - // Process died — clean up socket/pid but keep metadata 373 + // Process really died — clean up socket/pid but keep metadata 366 374 cleanupSocket(name); 367 375 } 368 376 } ··· 383 391 // this keys off exitedAt; for vanished sessions (no exit record written) 384 392 // fall back to createdAt so they don't accumulate indefinitely. A session 385 393 // with a missing daemon and a metadata file older than 24h is not coming 386 - // back regardless of why it died. 394 + // back regardless of why it died — *unless* the recorded pid is still 395 + // alive, in which case the daemon outlived its socket and we must keep 396 + // metadata around so the session stays addressable. 387 397 const ageAnchor = metadata.exitedAt ?? metadata.createdAt; 388 398 if (ageAnchor) { 389 399 const anchoredAt = new Date(ageAnchor).getTime(); 390 400 if (Date.now() - anchoredAt > DEAD_SESSION_TTL_MS) { 391 - cleanupAll(name); 392 - continue; 401 + const pid = readPid(name); 402 + if (pid === null || !isProcessAlive(pid)) { 403 + cleanupAll(name); 404 + continue; 405 + } 393 406 } 394 407 } 395 408
+52
tests/list-filters.test.ts
··· 155 155 }, 15000); 156 156 }); 157 157 158 + describe("listSessions guards against deleting state for live daemons", () => { 159 + // Refs https://github.com/myobie/pty/issues/34. listSessions used to 160 + // unconditionally `cleanupSocket` whenever the socket-reachable probe 161 + // failed and `cleanupAll` whenever metadata was older than 24h. Both 162 + // ran even if the recorded pid was still alive — once the .sock or 163 + // .json was gone, the still-running daemon became invisible to every 164 + // future scan. These tests pin the new behaviour: live pid wins. 165 + it("keeps a session whose socket file is missing but recorded pid is still alive", () => { 166 + const dir = makeSessionDir(); 167 + const name = uniqueName(); 168 + // Use the test runner's own pid as a stand-in for an alive daemon. 169 + fs.writeFileSync(path.join(dir, `${name}.pid`), String(process.pid)); 170 + fs.writeFileSync(path.join(dir, `${name}.json`), JSON.stringify({ 171 + command: "cat", args: [], displayCommand: "cat", cwd: os.tmpdir(), 172 + createdAt: new Date().toISOString(), 173 + })); 174 + // Note: no .sock file written. Without the guard, scan-and-cleanup paths 175 + // would fall into the .json branch and delete metadata-on-age. 176 + 177 + // Force the .json into the >24h bucket so the second guard is exercised. 178 + const old = new Date(Date.now() - 48 * 3600_000).toISOString(); 179 + fs.writeFileSync(path.join(dir, `${name}.json`), JSON.stringify({ 180 + command: "cat", args: [], displayCommand: "cat", cwd: os.tmpdir(), 181 + createdAt: old, 182 + })); 183 + 184 + const r = runCli(dir, "list", "--json"); 185 + expect(r.status).toBe(0); 186 + const found = JSON.parse(r.stdout).find((s: any) => s.name === name); 187 + expect(found, "session should still be listed because its pid is alive").toBeDefined(); 188 + // Metadata file must survive the call so the next scan also sees it. 189 + expect(fs.existsSync(path.join(dir, `${name}.json`))).toBe(true); 190 + }, 10000); 191 + 192 + it("does delete metadata older than 24h when the pid is dead", () => { 193 + const dir = makeSessionDir(); 194 + const name = uniqueName(); 195 + // Pid 0x7fffffff is "guaranteed dead" on Linux/macOS in practice. 196 + fs.writeFileSync(path.join(dir, `${name}.pid`), "2147483647"); 197 + const old = new Date(Date.now() - 48 * 3600_000).toISOString(); 198 + fs.writeFileSync(path.join(dir, `${name}.json`), JSON.stringify({ 199 + command: "cat", args: [], displayCommand: "cat", cwd: os.tmpdir(), 200 + createdAt: old, 201 + })); 202 + 203 + const r = runCli(dir, "list", "--json"); 204 + expect(r.status).toBe(0); 205 + expect(JSON.parse(r.stdout).find((s: any) => s.name === name)).toBeUndefined(); 206 + expect(fs.existsSync(path.join(dir, `${name}.json`))).toBe(false); 207 + }, 10000); 208 + }); 209 + 158 210 describe("pty list --status", () => { 159 211 it("filters to a single status", async () => { 160 212 const dir = makeSessionDir();