cd /news/ai-agents/show-hn-watch-all-the-ai-agents-on-y… · home topics ai-agents article
[ARTICLE · art-136915] src=github.com ↗ pub= topic=ai-agents verified=true sentiment=· neutral

Show HN: Watch all the AI agents on your machine

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.

read11 min views1 publishedSep 22, 2026
Show HN: Watch all the AI agents on your machine
Image: Michielbdejong (auto-discovered)

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, soactivity.error is usually empty.
  4. grok -p registers as live only whenGROK_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 readsidle until its transcript appears.
  7. A pending ask tool readswaiting . Permission approvals never reach disk, so a session blocked on one readsrunning (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 readsinteractive . Piped or detached, it has no terminal and appears in history only. Nothing on disk marks a runheadless .
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). 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 orEsc 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.

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: busyrunning, waitingwaiting, shellwaiting with waitingFor: 'shell' (the turn ended but a background shell will wake the session), idleidle. 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_executionrunning; permission_promptwaiting (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_startedrunning; task_complete / turn_abortedidle. 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 toolUserunning; stop / lengthidle; erroridle with the turn failed and its errorMessage; abortedidle, 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.

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.

── more in #ai-agents 4 stories · sorted by recency
── more on @mark wylde 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/show-hn-watch-all-th…] indexed:0 read:11min 2026-09-22 ·