# Show HN: Watch all the AI agents on your machine

> Source: <https://github.com/markwylde/all-your-agents>
> Published: 2026-09-22 10:57:49+00:00

Watch every coding agent on this machine, and inspect the sessions they leave behind.

Subagents covers start, end, background and nested.

1. `claude -p` writes no live index entry, so a print-mode run appears in history only.
2. Grok's `tool_started` carries no call id, so the current tool is identified by name. Two tools with the same name running at once show as one.
3. A failed turn is reported as `failed` , but Grok records no error message for it, so`activity.error` is usually empty.
4. `grok -p` registers as live only when`GROK_TRACK_HEADLESS` is set. Otherwise it appears in history only.
5. Codex does not persist approval requests, so a session blocked on an approval reads as `running` . A launched Codex with no prompt yet has no rollout. A`/resume` after watch started appears on its first append. A VS Code thread unloaded without touching another file stays listed until the next event for that pid.
6. omp writes no transcript until the first reply of a new session ends. That first turn is read from omp's prompt history, which needs SQLite: Node ≥ 22.13, or your own `sqlite` reader. Without it a new session reads`idle` until its transcript appears.
7. A pending `ask` tool reads`waiting` . Permission approvals never reach disk, so a session blocked on one reads`running` (omp's default approval mode asks for none).
8. omp names a run after the terminal on its stdin, in print mode too: `omp -p` typed in a terminal is live and reads`interactive` . Piped or detached, it has no terminal and appears in history only. Nothing on disk marks a run`headless` .

``` python
import AllYourAgents, { builtInProviders } from '@markwylde/all-your-agents';

const aya = AllYourAgents({
  providers: [...builtInProviders],
});

aya.on('session:create', (session) => {
  console.log(session.harness, session.id, session.pid);
});

aya.start();
```

No polling, ever ([ADR 0001](https://github.com/markwylde/all-your-agents/blob/main/docs/adr/0001-never-poll.md)). `start()` watches the files each harness already writes and the processes that write them. Bursts are coalesced per path (about 25 ms quiet, at most 1 s behind). `stop()` drops every file and process watch. On macOS, where opening one `fs.watch` can make the others miss an event, each watch re-checks what it covers once after any watch opens or closes (see the ADR); a custom `fs` can opt in with `onWatchChurn`.

```
npm install @markwylde/all-your-agents
```

Node.js ≥ 20, macOS and Linux. Pass `{ fs, processes }` to observe a remote machine.

Run straight from source on Node ≥ 22.18 (no build step):

```
node ./demo/list.ts          # every session, live and historical, as a table
node ./demo/list.ts --live   # only live sessions
node ./demo/watch.ts         # stream session events as they happen (Ctrl+C to stop)
```

`all-your-agents` is `top` for coding agents. It lists every live session, puts the ones waiting on you first, and updates as they change. Press `H` for every session on the machine, finished ones included, and `t` to read what any of them said.

```
npx @markwylde/all-your-agents            # full-screen live view
npx @markwylde/all-your-agents --once     # print a table and exit (also when piped)
npx @markwylde/all-your-agents --json     # print live sessions as JSON and exit
npx @markwylde/all-your-agents --all      # also show sessions that close while it is open
npx @markwylde/all-your-agents --history  # start with every session, not only live ones
npx @markwylde/all-your-agents --json --history   # every session as JSON, live ones first
```

| Key | Action | 
|---|---|
| `↑``↓` /`k`` j` ,`Home`` End` ,`PgUp`` PgDn` | Move the selection | 
| `Enter` | Details: full title and folder, what it is waiting for, current tool, last error, subagents | 
| `/` | Filter by title, folder, harness, model, or pid. `Esc` clears | 
| `s``>` /`<` ,`r` | Next / previous sort column, reverse | 
| `c` | Show or hide closed sessions | 
| `H` | Show or hide history: every session the providers know, in the same table | 
| `t` | Transcript of the selected session: prompts, replies, tools, outcomes. Follows a live session. `t` or`Esc` closes | 
| `?`` h` | Help | 
| `q`` Ctrl+C` | Quit and restore the terminal | 

The screen redraws only when an agent changes, a key is pressed, or the terminal resizes. History is read once when you press `H`; a transcript is one event stream, closed when you leave it. Times are clock times (`14:31:02`), not ticking durations, so nothing runs on a timer. `NO_COLOR` is honoured.

Live sessions have a `pid`. Historical ones do not. `kind` is `interactive` or `headless`.

``` python
import type { Session, SessionActivity, Subagent, Turn, SessionEvent } from '@markwylde/all-your-agents';
```

`session.activity` is the current or last turn (`tool`, `lastTurn`, `error`, `openSubagents`), separate from `status`. `session.subagents()` lists launched subagents. `transcript()` yields `Turn` s; `events()` tails normalized `SessionEvent` s until you stop iterating or call `close()` on it (both work while it is waiting) (`user`, `assistant`, `tool`, `tool-result`, `title`, `turn-end`, `subagent`, `subagent-end`, `error`, `other`). Each item keeps the original record on `raw`.

**Two kinds of waiting.** `status: 'waiting'` means either that the session is blocked on you (a permission prompt, a question), or that its turn is over while a background shell command or monitor it started is still running and will wake it. The second kind always has `waitingFor` `'shell'` or `'monitor'`, so `session.status === 'waiting' && session.waitingFor !== 'shell' && session.waitingFor !== 'monitor'` is "needs the user". Such a session is not `idle` until that work ends; its finished turn is on `activity.lastTurn` meanwhile. A background subagent on its own leaves the session `idle` and shows on `subagents()`.

**Title precedence**, highest first: `user` (custom title) > `harness` (generated) > `process` (session-file name) > `prompt` (first real user prompt). `session:update` fires only when the effective title, cwd, or model changes.

| Event | When | 
|---|---|
| `session:create` | A live process owns a conversation that did not already have a journal. | 
| `session:open` | A live process owns a conversation that already had a journal. | 
| `session:status` | Status changed ( `running` \|`waiting` \|`idle` ). | 
| `session:update` | Effective title, cwd, or model changed. | 
| `session:activity` | `session.activity` changed. | 
| `session:close` | The process no longer holds it. `pid` is unset; history remains. | 
| `subagent:start` /`subagent:end` | A subagent launched or finished. | 
| `ready` | Catch-up from `start()` finished. | 
| `error` | A provider failed ( `{ source: 'provider', provider, error }` ) or one of your listeners threw (`{ source: 'listener', event, error }` ). Never thrown into the library. | 

Subscribe before `start()`. Catch-up events carry `{ catchUp: true }`, then `ready`, then `{ catchUp: false }`.

A listener that throws never stops updates. Register an `error` listener to receive the failure; without one it is rethrown asynchronously as an uncaught exception, so it is never lost.

```
aya.running();
await aya.sessions({ harness, cwd, live, kind, since });
await aya.get(id);
await aya.reconcile(pid); // one-shot re-validation; never schedules
```

`since` is epoch ms matched against `updatedAt` (fallback `startedAt`). `kind` is `interactive` | `headless`. `sessions()` works without `start()`. `transcript()`, `events()` and `subagents()` are always served by the provider named in `session.provider`. Closed sessions stay in memory for the 1000 most recent; older ones come from their provider's history.

```
AllYourAgents({
  providers: [...builtInProviders],
  fs,            // default: local filesystem
  processes,     // default: local ps/proc + kqueue/pidfd via optional koffi
  sqlite,        // default: node:sqlite where the runtime has it; `false` for none
  debounce: { quietMs: 25, maxLatencyMs: 1000 },
});
```

`reconcile(pid?)` is for hosts that already know a process exited (a terminal emulator). Without `koffi`, `processes.watch` is `unsupported` and each provider re-validates on the next change to its live index or on `reconcile`.

Provider id `claude-code`, harness `ClaudeCode`. Home is `$CLAUDE_CONFIG_DIR` if set, otherwise `~/.claude`, overridable via `claudeCode({ home })`.

Live index: `<home>/sessions/<pid>.json`. Journals: `<home>/projects/<encoded-cwd>/<id>.jsonl`. Every non-alphanumeric character in the cwd becomes `-`, so `/Users/me/app/.claude/worktrees/x` is `-Users-me-app--claude-worktrees-x`.

Status: `busy` → `running`, `waiting` → `waiting`, `shell` → `waiting` with `waitingFor: 'shell'` (the turn ended but a background shell will wake the session), `idle` → `idle`. Unknown words omit `status`. `model` is the model of the latest assistant record in the journal. `.key` files and the messaging socket are never opened.

Provider id `grok-build`, harness `Grok`. Home is `$GROK_HOME` if set, otherwise `~/.grok`, overridable via `grokBuild({ home })`. All four built-ins are in `builtInProviders`; pass `providers: [claudeCode()]` to watch Claude Code only.

Live index: `<home>/active_sessions.json`, an array of `{ session_id, pid, cwd, opened_at }`. One pid can hold several sessions. `grok -p` registers only when `GROK_TRACK_HEADLESS` is set; otherwise print-mode runs appear in history with `kind` `headless`. Sessions: `<home>/sessions/<encoded-cwd>/<id>/`. The cwd is percent-encoded like Rust `urlencoding` (everything but `A-Za-z0-9-._~`, so `/tmp/foo(bar)!` is `%2Ftmp%2Ffoo%28bar%29%21`); a cwd whose encoding exceeds 255 bytes is found by a one-level lookup for the session id.

Status comes from `events.jsonl`, Grok's phase log: `waiting_for_model`, `streaming_text`, `streaming_reasoning`, `tool_execution` → `running`; `permission_prompt` → `waiting` (with the tool named by `permission_requested`); no open turn → `idle`, or `waiting` while a background task is still running, with `waitingFor` `monitor` if one of them is a monitor and `shell` otherwise. Those tasks are the only thing read from `updates.jsonl`: its `_x.ai/session/update` rows `background_tasks` and `task_completed`; every other row is skipped unparsed. Unknown phases omit `status`. Titles and `model` come from `summary.json`, the conversation from `chat_history.jsonl`, subagents from `subagents/<id>/meta.json`. `active_sessions.lock`, `*.tmp`, `auth.json` and the session-search sqlite are never opened.

Provider id `codex-cli`, harness `Codex`. Home is `$CODEX_HOME` if set, otherwise `~/.codex`, overridable via `codexCli({ home })`. VS Code Codex sessions that share that home are the same provider.

Live sessions are rollout files a process currently has open: `<home>/sessions/YYYY/MM/DD/rollout-<timestamp>-<thread-id>.jsonl`. There is no pid index. Status comes from persisted turn lifecycle events: `task_started` → `running`; `task_complete` / `turn_aborted` → `idle`. Codex does not persist approvals, and has no background work that wakes an ended turn (a unified exec process that outlives its turn is not recorded as running), so `waiting` is never reported. Titles come from `session_index.jsonl` then the first user prompt; the conversation from `response_item` records; subagents from child rollouts with `parent_thread_id`. A compressed `.jsonl.zst` thread is listed only when `session_index.jsonl` has a title for it, so compressed children are not treated as sessions (an unnamed compressed root is omitted). `auth.json`, `state_*.sqlite`, `ipc/`, `process_manager/chat_processes.json` and `history.jsonl` are never opened.

A session does not exist until the first prompt writes a rollout. A resume after watch started appears on the first append. A quietly unloaded VS Code thread stays listed until the next event for that pid.

Provider id `oh-my-pi`, harness `OhMyPi`. Home is `~/.omp` (or `~/$PI_CONFIG_DIR`), overridable via `ohMyPi({ home })`. `PI_CODING_AGENT_DIR` and, once `$XDG_DATA_HOME/omp` / `$XDG_STATE_HOME/omp` exist, the XDG locations are followed as omp follows them. Every named profile under `<home>/profiles/`, or under `$XDG_DATA_HOME/omp/profiles` / `$XDG_STATE_HOME/omp/profiles`, is observed too, and a deleted one closes its sessions. Upstream `pi` (`~/.pi`) is not.

omp keeps a registry in two parts. Each process writes `run/daemons/<project-hash>/clients/<pid>-<uuid>.json` at launch and removes it on a clean exit. Each process on a terminal writes `agent/terminal-sessions/<tty>` naming the session file it is on, rewritten when you switch session (`/new`, `/resume`) and marked `fresh` while that file does not exist yet. The two are joined by the process's controlling terminal, read once per process with `processes.tty(pid)`. A breadcrumb older than the process on its terminal is not that process's, and binds nothing.

Transcripts: `agent/sessions/<encoded-cwd>/<timestamp>_<id>.jsonl`. The directory name is lossy, so `cwd` comes from the header. A session kept elsewhere (`--session notes/work`, `--session-dir`) is found through `agent/custom-session-files/`, and is bound from its breadcrumb once its file exists; omp gives a file not named `*.jsonl` no subagents. Status comes from the last conversation record: a user message, a tool result, or an assistant `stopReason` of `toolUse` → `running`; `stop` / `length` → `idle`; `error` → `idle` with the turn `failed` and its `errorMessage`; `aborted` → `idle`, `interrupted`. With no turn open, a `bash` result that omp backgrounded (`details.async`, `state: running`) makes that `waiting` with `waitingFor` `shell` until an `async-result` message or a later result reports the job; a job left by an earlier process is dropped. A turn omp retries after a provider error starts again without a new prompt. Tools come from `toolCall` blocks and their `toolResult`; titles from `title_change`; `model` from `model_change` and the latest reply. Subagents run inside the parent process and are read from `<session>/<AgentId>.jsonl` (nested ones from `<session>/<Parent>/<Parent>.<Child>.jsonl`): type from `session_init.agent`, done when the child records a successful `yield`, `background` when the `task` result said it spawned them asynchronously. For a finished session, a child whose transcript never says how it ended takes its parent's report of it, else `cancelled`.

Only the `history` table of `agent/history.db` is read, and only for a session with no transcript yet. `agent.db`, `models.db`, `logs/`, `blobs/` and the `.lock` sidecars are never opened.

``` js
import { defineConformanceTests, createMemoryHarness } from '@markwylde/all-your-agents/testing';

const { provider, driver } = createMemoryHarness();
defineConformanceTests({ name: 'memory', provider, driver });
```

`createClaudeFixtureDriver(home)`, `createGrokFixtureDriver(home)`, `createCodexFixtureDriver(home)` and `createOmpFixtureDriver(home)` drive the same kit against those providers. A driver's optional `startBackgroundWait` / `endBackgroundWait` steps turn on the background-wait case; leave them out for a harness that records no such work.

Launching agents, IPC sockets, inferring status from journals, Windows.
