{"slug": "show-hn-watch-all-the-ai-agents-on-your-machine", "title": "Show HN: Watch all the AI agents on your machine", "summary": "Developer Mark Wylde released @markwylde/all-your-agents, an open-source tool that watches every coding agent session on a machine without polling, tracking harnesses including Claude, Grok, Codex, and omp. The package requires Node.js 20 or later on macOS and Linux and is installed via npm, with a full-screen live view available through npx. The tool lists live sessions, surfaces sessions waiting on user input first, and lets users press H for finished sessions and t to read their transcripts.", "body_md": "Watch every coding agent on this machine, and inspect the sessions they leave behind.\n\nSubagents covers start, end, background and nested.\n\n1. `claude -p` writes no live index entry, so a print-mode run appears in history only.\n2. 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.\n3. A failed turn is reported as `failed` , but Grok records no error message for it, so`activity.error` is usually empty.\n4. `grok -p` registers as live only when`GROK_TRACK_HEADLESS` is set. Otherwise it appears in history only.\n5. 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.\n6. 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.\n7. 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).\n8. 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` .\n\n``` python\nimport AllYourAgents, { builtInProviders } from '@markwylde/all-your-agents';\n\nconst aya = AllYourAgents({\n  providers: [...builtInProviders],\n});\n\naya.on('session:create', (session) => {\n  console.log(session.harness, session.id, session.pid);\n});\n\naya.start();\n```\n\nNo 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`.\n\n```\nnpm install @markwylde/all-your-agents\n```\n\nNode.js ≥ 20, macOS and Linux. Pass `{ fs, processes }` to observe a remote machine.\n\nRun straight from source on Node ≥ 22.18 (no build step):\n\n```\nnode ./demo/list.ts          # every session, live and historical, as a table\nnode ./demo/list.ts --live   # only live sessions\nnode ./demo/watch.ts         # stream session events as they happen (Ctrl+C to stop)\n```\n\n`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.\n\n```\nnpx @markwylde/all-your-agents            # full-screen live view\nnpx @markwylde/all-your-agents --once     # print a table and exit (also when piped)\nnpx @markwylde/all-your-agents --json     # print live sessions as JSON and exit\nnpx @markwylde/all-your-agents --all      # also show sessions that close while it is open\nnpx @markwylde/all-your-agents --history  # start with every session, not only live ones\nnpx @markwylde/all-your-agents --json --history   # every session as JSON, live ones first\n```\n\n| Key | Action | \n|---|---|\n| `↑``↓` /`k`` j` ,`Home`` End` ,`PgUp`` PgDn` | Move the selection | \n| `Enter` | Details: full title and folder, what it is waiting for, current tool, last error, subagents | \n| `/` | Filter by title, folder, harness, model, or pid. `Esc` clears | \n| `s``>` /`<` ,`r` | Next / previous sort column, reverse | \n| `c` | Show or hide closed sessions | \n| `H` | Show or hide history: every session the providers know, in the same table | \n| `t` | Transcript of the selected session: prompts, replies, tools, outcomes. Follows a live session. `t` or`Esc` closes | \n| `?`` h` | Help | \n| `q`` Ctrl+C` | Quit and restore the terminal | \n\nThe 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.\n\nLive sessions have a `pid`. Historical ones do not. `kind` is `interactive` or `headless`.\n\n``` python\nimport type { Session, SessionActivity, Subagent, Turn, SessionEvent } from '@markwylde/all-your-agents';\n```\n\n`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`.\n\n**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()`.\n\n**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.\n\n| Event | When | \n|---|---|\n| `session:create` | A live process owns a conversation that did not already have a journal. | \n| `session:open` | A live process owns a conversation that already had a journal. | \n| `session:status` | Status changed ( `running` \\|`waiting` \\|`idle` ). | \n| `session:update` | Effective title, cwd, or model changed. | \n| `session:activity` | `session.activity` changed. | \n| `session:close` | The process no longer holds it. `pid` is unset; history remains. | \n| `subagent:start` /`subagent:end` | A subagent launched or finished. | \n| `ready` | Catch-up from `start()` finished. | \n| `error` | A provider failed ( `{ source: 'provider', provider, error }` ) or one of your listeners threw (`{ source: 'listener', event, error }` ). Never thrown into the library. | \n\nSubscribe before `start()`. Catch-up events carry `{ catchUp: true }`, then `ready`, then `{ catchUp: false }`.\n\nA 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.\n\n```\naya.running();\nawait aya.sessions({ harness, cwd, live, kind, since });\nawait aya.get(id);\nawait aya.reconcile(pid); // one-shot re-validation; never schedules\n```\n\n`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.\n\n```\nAllYourAgents({\n  providers: [...builtInProviders],\n  fs,            // default: local filesystem\n  processes,     // default: local ps/proc + kqueue/pidfd via optional koffi\n  sqlite,        // default: node:sqlite where the runtime has it; `false` for none\n  debounce: { quietMs: 25, maxLatencyMs: 1000 },\n});\n```\n\n`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`.\n\nProvider id `claude-code`, harness `ClaudeCode`. Home is `$CLAUDE_CONFIG_DIR` if set, otherwise `~/.claude`, overridable via `claudeCode({ home })`.\n\nLive 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`.\n\nStatus: `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.\n\nProvider 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.\n\nLive 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.\n\nStatus 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.\n\nProvider 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.\n\nLive 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.\n\nA 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.\n\nProvider 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.\n\nomp 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.\n\nTranscripts: `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`.\n\nOnly 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.\n\n``` js\nimport { defineConformanceTests, createMemoryHarness } from '@markwylde/all-your-agents/testing';\n\nconst { provider, driver } = createMemoryHarness();\ndefineConformanceTests({ name: 'memory', provider, driver });\n```\n\n`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.\n\nLaunching agents, IPC sockets, inferring status from journals, Windows.", "url": "https://wpnews.pro/news/show-hn-watch-all-the-ai-agents-on-your-machine", "canonical_source": "https://github.com/markwylde/all-your-agents", "published_at": "2026-09-22 10:57:49+00:00", "updated_at": "2026-09-22 11:24:51.771236+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "ai-tools"], "entities": ["Mark Wylde", "@markwylde/all-your-agents", "Claude", "Grok", "Codex", "omp", "npm", "Node.js"], "alternates": {"html": "https://wpnews.pro/news/show-hn-watch-all-the-ai-agents-on-your-machine", "markdown": "https://wpnews.pro/news/show-hn-watch-all-the-ai-agents-on-your-machine.md", "text": "https://wpnews.pro/news/show-hn-watch-all-the-ai-agents-on-your-machine.txt", "jsonld": "https://wpnews.pro/news/show-hn-watch-all-the-ai-agents-on-your-machine.jsonld"}}