# list-sessions tool

audience: AI coding agents first.

status: DONE
task IDs: #1, #2, #3, #4, #5
source request: Owner: "i want you to create a script that i can always use, call it 'list-sessions' and it should do it the same way i asked" — breakdown of every live Claude Code session on the machine: title/id per session, PID, attached/detached/headless, currently active, last active time, grouped. Owner: "not in local bin. put it in overdeck where it belongs". Phase 2 — Owner: "i want to have a --type/-t [attached,detached] flag and --status/-s [busy,idle,waiting] --cwd/-c [name] so i can filter results easily from cli". Phase 3 — Owner: "i want to have also date started and last attached columns in list-sessions", then corrected: "sorry, last DETACHED. not attached". Owner chose (question 2026-08-13): tmux records attach time, not detach time → "Use last-attach as proxy": column shows the last time the session was attached (closest provable value), attached sessions show "-". Phase 4 — Owner: "add a --kill flag for me, when i want to kill all sessions that are both detached AND idle". Phase 5 — Owner: "i want list-sessions to also show the MEM/CPU usage of each of the processes and mark excessive users with a color".

## Outcome

A single `list-sessions` command, installed from `modules/workstation/claude/bin/list-sessions` (deploy-clone `~/.claude/bin` symlink = already on PATH), that prints one table of all live Claude Code sessions grouped by project, without root or extra tools.

## Acceptance criteria

- Enumerates every live claude session process and only those (bwrap/pty-host plumbing excluded).
- Per session: conversation name, session id, pid, state (attached/detached/headless), status (busy/idle/waiting), last-activity time, cwd.
- Attachment resolved from tmux client state on the human-session socket; falls back to tty/dtach.
- Last activity = transcript file mtime (ground truth), fallback session-state `updatedAt`.
- Groups by project (first path segment under `~/Projects`, `~` otherwise), projects sorted by newest activity; summary line.
- Pure python3 stdlib; executes on this machine within ~2s; no writes to any state.

## Preserved WIP / ref

- Session state: `~/.claude/sessions/<pid>.json` (sessionId, name, cwd, status, updatedAt, kind, tmux target).
- Transcripts: `~/.claude/projects/*/<sessionId>.jsonl` (title = first user message; activity = mtime).
- tmux server: `~/.local/state/human-session/tmux.sock` — `list-sessions -F` gives per-session attached flag.
- Old sessions (no state file): argc `--resume <uuid|name>` / `--session-id <uuid>`, cwd from `/proc/<pid>/cwd`.

## Constraints

- Do not touch the shared main checkout; work in `.worktrees/list-sessions`; land via `ship.sh land`.
- Disk admission floor overridden by owner (8 GB enough): `OD_WORKTREE_MIN_FREE_KIB=7340032`.
- Disk-freeing beyond this task is paused per owner.
- No dependencies (stdlib only), no writes to any state file (read-only queries).

## Execution steps

1. Implement `modules/workstation/claude/bin/list-sessions` in worktree. ✓ plans + index row.
2. Chmod +x; run against live machine; verify every live session present, plumbing excluded, no crash on old-style sessions.
3. Install live: copy landed file into deploy clone `~/.local/share/overdeck/deploy/.../bin/` and run from PATH.
4. Land `wt/list-sessions` via `.claude/scripts/ship.sh land` (POSTLANDCMD runs deploy-local.sh), verify deployed copy.
5. Owner-visible receipt: `list-sessions` output + confirmation it lives on PATH.

## Phase 3 — STARTED + DETACHED columns (landed)

- Requirement: add two columns — session start date and last detached. Owner corrected detached; chose last-attach proxy (tmux has no detach stamp; `session_last_detached` empty on tmux 3.7b).
- Semantics: STARTED = state `startedAt` (epoch ms), fallback `/proc` process start; formatted `MM-DD HH:MM` (year shown for >1y). DETACHED = tmux `session_last_attached` when the session is currently detached (proxy for unattended-since); "-" when currently attached, headless, or non-tmux (no data — never fabricated). Same values in `--json` (`started`, `detached` strings).
- Acceptance: attached session → "-" in DETACHED; detached tmux session → its last-attach time; started equals state startedAt; filters unchanged.
- Verified: detached row `1c7edc98` showed `08-12 13:31`, cross-checked against raw `tmux list-sessions #{session_last_attached}` → exact match; all attached/headless rows "-"; filters (`-t`/`-s`/`-c`) unaffected.

## Phase 4 — --kill flag (in progress)

- Requirement: `--kill` terminates every session that is BOTH detached AND idle (owner's exact predicate) — one command to clean up zombie unattended sessions.
- Semantics: `--kill` switches to action mode — no table, prints one `killed <pid> <title>` line per victim plus summary (`Killed N of M matched` + remaining count). Fixed predicate (state starts with `detached` — includes dtach — AND status `idle`); listing filters `-t/-s/-c` are ignored in kill mode (YAGNI; owner asked for exactly this set). SIGTERM only (graceful, Claude Code persists state); re-verify `/proc/<pid>/exe` is a claude binary immediately before signaling (pid-reuse race); never kill own PID (`os.getpid()` guard); dead/already-gone PIDs reported as skipped, not errors. With `--json`, emit `{"killed": [...], "skipped": [...], "remaining": N}`. Exit 0 always.
- Acceptance: detached+idle sessions die; detached+busy, attached+idle, headless untouched; no victims → "Killed 0 of 0"; help text documents the predicate.

## Phase 4 receipt

## Phase 4 receipt

Phase 4 landed `73196989` (tool commit `d5f059a71`); deployed automatically on land (`deploy-local` status `deployed`, sha `73196989`) — `/logs` 200, clone pristine, `list-sessions --kill` live on PATH. Verified end-to-end with a real throwaway zombie: spawned a genuine claude process in a detached tmux session, gave it a harness-shaped state fixture (exact state-file shape with tmux target), predicate isolated it as the only victim of 38 rows, `--kill` SIGTERMed it (`killed 1336034 killtest`, `Killed 1 of 38 (0 skipped); 37 remain`, exit 0), process confirmed gone, all real sessions untouched (38 → 37), fixtures/tmux cleaned up. Zero-victim runs verified too (`Killed 0 of 37`) and `--kill --json` shape `{"killed":[],"skipped":[],"remaining":N}`. Note: a bare `claude` launch has no harness state file → classifies as attached via tty fallback, so real zombies (harness-launched, tmux-detached, state file present) are the ones the flag targets.

## Phase 3 receipt

## Current receipt

Phase 1 landed `852985795` (tool commit `ea013048c`); phase-1 receipt landed `c3b9d3f67` (ticket delayed, content safe). Phase 2 (filters) landed `b882cd077` (commits `5d1b81fbd` + conflict-resolution `aac642d45`) after one candidate-conflict vs the landed receipt — resolved by taking the phase-2 docs wholesale (superset). Tool live on PATH `~/.claude/bin/list-sessions` (install-before-land); filters verified against live 58-session state: `-t` splits 31 detached/26 attached/1 headless, `-s busy`=8, `-c` case-insensitive substring, AND-combination, `--json` parity, argparse rejects bad values.

DELIVERY COMPLETE — deploy succeeded 2026-08-13 17:31: `{"stage":"deploy-local","status":"deployed","sha":"eba31203","web":"http://127.0.0.1:31337","collector_http":"401"}`. Blocker sequence: factory-k3s session landed its engine lib (now `eba31203` = `harness: land wt/factory-k3s-receipt-land`); bounded watcher (`deploy-watch`, systemd transient, bounded 180m) verified byte-identity, relocated the blocking files to `/home/user/.local/share/overdeck/rescue-watch-20260813T153320/` (moved, never deleted; kept for recovery), then the official `bash packaging/deploy-local.sh` passed. Installed-route proof: `GET http://127.0.0.1:31337/logs -> 200`; collector auth-gated by design (deploy's own readiness treats any non-000 as healthy; `401` on unauthenticated probes is expected); `overdeck-web.service` + `overdeck-collector.service` active (running) since 17:31; `factory-agent-k3s.mjs` + `list-sessions` present in the deployed clone; `list-sessions` live on PATH (32 sessions at last check). Honest limitation: web release reused the `.build-a92556d49…` artifact via the no-web-change fast path (deploy diff since that release touched only workstation/claude + docs).

## Phase 2 — CLI filters (landed)

- Requirement: `--type/-t [attached,detached]`, `--status/-s [busy,idle,waiting]`, `--cwd/-c [cwd substring]`; filter the table from the CLI.
- Semantics: type is exact-state (values also include the existing `headless` state — sessions that are neither attached nor detached; `-t` accepts it too); multiple `-t`/`-s`/`-c` accumulate as OR within a flag, AND across flags; `-c` matches case-insensitive substring of the cwd path; filters apply identically to `--json`; group headers with zero matches are dropped; summary line shows `N of M sessions` when filtered (honest counts).
- Acceptance (verified): `-t attached` shows only attached rows; `-s busy` only busy; `-c <string>` only paths containing it; combined flags AND; unknown value → argparse error (exit 2).

Phase 3 landed `47ff66932` (tool commit `870b51103`); deployed `f46e413f` (`deploy-local` status `deployed-coalesced`, 2026-08-13 20:06) — web `http://127.0.0.1:31337/logs` → 200, `overdeck-web.service` + `overdeck-collector.service` active (running), clone pristine, `list-sessions` on PATH showing STARTED + DETACHED columns. Deploy gate note: post-land auto-deploy initially refused `deploy-clone-dirty` — the dirty file was my own install-before-land copy, byte-identical (md5 `06dd602d…`) to the landed blob; restored pristine (lossless), deploy then passed via queue (coalesced).

## Phase 5 — per-process CPU/MEM columns with excessive marked red (landed)

- Requirement: show MEM/CPU usage of each listed process; mark excessive users with a color.
- Semantics: CPU% is INSTANTANEOUS, not lifetime average (a lifetime average hides a runaway that started recently): two jiffy samples of `/proc/<pid>/stat` utime+stime taken 1.0s apart (`SAMPLE_WINDOW`), divided by machine jiffies from `/proc/stat` (user..steal, guest excluded) times `ncpus` → % of one core (a process burning two cores shows ~200). MEM = `VmRSS` kB from `/proc/<pid>/status`. Sampling is skipped for `--kill` and when no rows exist; a process that dies mid-sample shows `-`/null. `--json` gains `cpu_pct` + `mem_kb` (null when unavailable). Colors are per-cell bold red when `cpu_pct >= CPU_EXCESSIVE (100.0)` or `mem_kb >= MEM_EXCESSIVE_KB (1 GiB)`; emitted ONLY on a terminal (`stdout.isatty()`) and suppressed when `NO_COLOR` is set — piped output carries no ANSI. Stdlib only, unchanged elsewhere (filters, kill, grouping).
- Acceptance: real CPU/RSS values per session; a pegging session shows red on a tty; piped/`--json` output is ANSI-free; `--kill` path takes no sampling delay.
- Verified live: table showed real values incl. a 121% busy session and 40% partial; forced-burn probe under a pty with a lowered threshold rendered red cells (escaped output confirmed `1;31`); piped run carried no ANSI; `--json` emitted `cpu_pct`/`mem_kb`; `py_compile` clean.

## Phase 5 receipt

Landed `4b95e31a7` (tool commit `4919d02b9`, worktree `wt/list-sessions-usage`); install-before-land kept the tool live the whole time (`~/.claude/bin` resolves INTO the deploy clone — same inode). The post-land auto-deploy failed at the dirty-clone check because the live edit IS the dirt (expected); the new adopt flow failed to self-clean: its re-pin does `git checkout --detach origin/main` BEFORE `git reset --hard`, and checkout refuses on dirty-but-byte-identical files (observed `checkout-failed "post-adoption re-pin failed"`); the reset itself would also trip the git-guard shim (observed on a manual attempt). Re-pinned the clone losslessly via `/usr/bin/git reset --hard origin/main` (byte-match of the single dirty file verified first; zero untracked files) → clone pristine at `4b95e31a7`, live file identical to landed blob. Second full deploy attempt then stopped at the documented pre-existing Actions Gateway R0 gate: `/home/user/.config/overdeck/actions-gateway.env` missing → failed closed with rollback, no endpoint installed (see `2026-08-14-chatgpt-operations-incremental-delivery.md`, Status BLOCKED, owner-provisioned secrets — do NOT synthesize). That gate blocks ANY full deploy of current main, unrelated to this change; the tool itself needs no deploy step. FLAGGED for the deploy/harness lane: the adopt-flow re-pin must tolerate byte-identical dirt (reset-only, or `/usr/bin/git`) — currently the mandated install-before-landing pattern can never self-heal the dirty-clone check.

## Next executable action

Feature: none — tool live and landed; the owner may run `list-sessions` and see CPU/MEM with red marking. Machine-wide deploy: BLOCKED by owner provisioning of `~/.config/overdeck/actions-gateway.env` (approved secret path, chatgpt-ops lane plan `2026-08-14-chatgpt-operations-incremental-delivery`). Deploy machinery: adopt-flow re-pin bug flagged above awaits the deploy/harness lane. Rescue dir `~/.local/share/overdeck/rescue-watch-20260813T153320/` may be pruned once the deployed clone is confirmed stable.