Show HN:监控机器上的所有 AI Agent
英文标题:🔶 Show HN: Watch all the AI agents on your machine
all-your-agents 是一个面向 Claude Code、Grok Build、Codex CLI 与 Oh My Pi 的 Node.js 工具和库,可实时发现、监控并回看一台机器上的编码 Agent 会话。它通过监听各 Agent 已写入的文件和相关进程实现无轮询监控,支持实时状态、模型、子 Agent、转录记录和历史会话。项目同时提供 API、全屏终端界面及 JSON 输出,开发者可用它排查 Agent 卡在权限确认、后台任务或工具调用等状态。该工具目前支持 macOS 和 Linux,Node.js 需 20 以上版本;部分 Agent 的无头模式、审批状态和转录数据仍受各自底层文件格式限制。
73.5 KB
中文
在这台机器上监看每一个编码 Agent,并检查它们留下的会话记录。
子 Agent 覆盖启动、结束、后台运行和嵌套等情况。
-
claude -p不会写入实时索引条目,因此打印模式运行仅会出现在历史记录中。 -
Grok 的
tool_started不带调用 ID,因此当前工具按名称识别。同时运行的两个同名工具会显示为一个。 -
失败的轮次会被报告为
failed,但 Grok 不会为其记录错误消息,因此activity.error通常为空。 -
仅当设置
GROK_TRACK_HEADLESS时,grok -p才会注册为实时会话;否则它仅出现在历史记录中。 -
Codex 不会持久化审批请求,因此被审批阻塞的会话会显示为
running。刚启动但尚未收到提示词的 Codex 没有 rollout。watch 启动后执行的/resume会在其首次追加内容时出现。未触碰其他文件便被卸载的 VS Code 线程,会一直列出到该 pid 的下一个事件发生为止。 -
omp 在新会话的首个回复结束前不会写入转录记录。首轮内容从 omp 的提示词历史中读取,这需要 SQLite:Node ≥ 22.13,或你自己的
sqlite读取器。没有它时,新会话在转录记录出现前会显示为idle。 -
待处理的
ask工具会显示为waiting。权限审批永远不会写入磁盘,因此被其阻塞的会话显示为running(omp 的默认审批模式不要求任何审批)。 -
即使在打印模式中,omp 也会根据其 stdin 所连接的终端为一次运行命名:在终端中输入的
omp -p是实时会话,并显示为interactive。通过管道运行或脱离终端运行时,它没有终端,只会出现在历史记录中。磁盘上没有任何内容会将一次运行标记为headless。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();
绝不轮询(ADR 0001)。start() 会监视每个 harness 已经写入的文件,以及写入这些文件的进程。每个路径上的事件突发会被合并处理(静默约 25 ms,延迟最多 1 秒)。stop() 会移除所有文件和进程监视。在 macOS 上,打开一个 fs.watch 可能导致其他监视遗漏事件,因此每次有监视打开或关闭后,每个监视都会重新检查一次其覆盖范围(见 ADR);自定义 fs 可通过 onWatchChurn 选择加入这一机制。
npm install @markwylde/all-your-agents
需要 Node.js ≥ 20,支持 macOS 和 Linux。传入 { fs, processes } 可观察远程机器。
在 Node ≥ 22.18 上直接从源码运行(无需构建步骤):
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 是编码 Agent 的 top。它列出全部实时会话,将正在等待你处理的会话置于最前,并会随其状态变化而更新。按 H 可查看机器上的所有会话,包括已完成会话;按 t 可阅读其中任一会话的内容。
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
| 按键 | 操作 |
|---|---|
↑ ↓ / k j、Home End、PgUp PgDn |
移动选择项 |
Enter |
详情:完整标题和文件夹、正在等待的内容、当前工具、最后一个错误、子 Agent |
/ |
按标题、文件夹、harness、模型或 pid 筛选。Esc 清除筛选 |
s > / <、r |
下一个/上一个排序列、反转排序 |
c |
显示或隐藏已关闭会话 |
H |
显示或隐藏历史:Provider 已知的所有会话,显示在同一张表中 |
t |
所选会话的转录记录:提示词、回复、工具、结果。会跟随实时会话。按 t 或 Esc 关闭 |
? h |
帮助 |
q Ctrl+C |
退出并还原终端 |
屏幕仅会在 Agent 发生变化、按下按键或终端大小变化时重绘。按下 H 时仅读取一次历史记录;转录记录是单一事件流,离开时关闭。时间显示为时钟时间(14:31:02),而非持续递增的时长,因此没有任何计时器在运行。遵守 NO_COLOR 设置。
实时会话具有 pid,历史会话则没有。kind 为 interactive 或 headless。
import type { Session, SessionActivity, Subagent, Turn, SessionEvent } from '@markwylde/all-your-agents';
session.activity 是当前或上一轮操作(tool、lastTurn、error、openSubagents),与 status 分离。session.subagents() 会列出已启动的子 Agent。transcript() 生成 Turn;events() 会持续读取已规范化的 SessionEvent,直到你停止迭代或对其调用 close()(二者在会话等待期间均可使用)(user、assistant、tool、tool-result、title、turn-end、subagent、subagent-end、error、other)。每一项都会在 raw 中保留原始记录。
两种等待状态。 status: 'waiting' 表示会话要么被你的操作阻塞(权限提示、提问),要么其当前轮次已经结束,但它启动的后台 shell 命令或监控任务仍在运行,并将在之后唤醒它。第二种情况总会具有 'shell' 或 'monitor' 的 waitingFor,因此 session.status === 'waiting' && session.waitingFor !== 'shell' && session.waitingFor !== 'monitor' 表示“需要用户处理”。此类会话在后台工作结束前不会变为 idle;在此期间,其已结束的轮次位于 activity.lastTurn。独立运行的后台子 Agent 会让会话保持 idle,并显示在 subagents() 中。
标题优先级,从高到低:user(自定义标题)> harness(生成的标题)> process(会话文件名)> prompt(第一个真正的用户提示词)。仅当有效标题、cwd 或模型发生变化时才会触发 session:update。
| 事件 | 触发时机 |
|---|---|
session:create |
一个实时进程拥有此前没有 journal 的对话。 |
session:open |
一个实时进程拥有此前已有 journal 的对话。 |
session:status |
状态发生变化(running |
session:update |
有效标题、cwd 或模型发生变化。 |
session:activity |
session.activity 发生变化。 |
session:close |
进程不再持有该会话。pid 被取消设置;历史记录仍会保留。 |
subagent:start / subagent:end |
一个子 Agent 启动或结束。 |
ready |
start() 的追赶同步完成。 |
error |
Provider 失败({ source: 'provider', provider, error }),或你的某个监听器抛出错误({ source: 'listener', event, error })。错误绝不会抛入库内部。 |
请在 start() 之前订阅。追赶同步事件带有 { catchUp: true },随后是 ready,再之后为 { catchUp: false }。
抛出错误的监听器绝不会停止更新。注册 error 监听器即可接收失败信息;如果未注册,它会以未捕获异常的形式异步重新抛出,因此错误不会丢失。
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 是与 updatedAt(回退为 startedAt)匹配的 epoch 毫秒值。kind 为 interactive | headless。sessions() 无需 start() 也可使用。transcript()、events() 和 subagents() 始终由 session.provider 指定的 Provider 提供服务。最近关闭的 1000 个会话会保留在内存中;更早的会话从各自 Provider 的历史记录中获取。
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?) 面向已经知道某个进程退出的宿主程序使用,例如终端模拟器。没有 koffi 时,processes.watch 为 unsupported,每个 Provider 会在其实时索引的下一次变化时,或在调用 reconcile 时重新验证。
Provider ID 为 claude-code,harness 为 ClaudeCode。Home 目录在设置 $CLAUDE_CONFIG_DIR 时为该路径,否则为 ~/.claude,可通过 claudeCode({ home }) 覆盖。
实时索引:<home>/sessions/<pid>.json。Journal:<home>/projects/<encoded-cwd>/<id>.jsonl。cwd 中的每个非字母数字字符都会变为 -,因此 /Users/me/app/.claude/worktrees/x 会变为 -Users-me-app--claude-worktrees-x。
状态映射:busy → running,waiting → waiting,shell → 带有 waitingFor: 'shell' 的 waiting(当前轮次已结束,但后台 shell 会唤醒该会话),idle → idle。未知词不会设置 status。model 是 journal 中最新 assistant 记录的模型。绝不会打开 .key 文件和消息传递 socket。
Provider ID 为 grok-build,harness 为 Grok。Home 目录在设置 $GROK_HOME 时为该路径,否则为 ~/.grok,可通过 grokBuild({ home }) 覆盖。四个内置 Provider 均位于 builtInProviders;传入 providers: [claudeCode()] 则只监控 Claude Code。
实时索引:<home>/active_sessions.json,内容是 { session_id, pid, cwd, opened_at } 数组。一个 pid 可以持有多个会话。仅在设置 GROK_TRACK_HEADLESS 时,grok -p 才会注册;否则打印模式运行会以 headless 的 kind 出现在历史记录中。会话路径:<home>/sessions/<encoded-cwd>/<id>/。cwd 使用类似 Rust urlencoding 的百分号编码(除 A-Za-z0-9-._~ 外全部编码,因此 /tmp/foo(bar)! 为 %2Ftmp%2Ffoo%28bar%29%21);若 cwd 编码结果超过 255 字节,则通过按会话 ID 进行单层查找来定位。
状态来自 Grok 的阶段日志 events.jsonl:waiting_for_model、streaming_text、streaming_reasoning、tool_execution → running;permission_prompt → waiting(并附带由 permission_requested 指定名称的工具);没有未结束轮次时为 idle,若后台任务仍在运行则为 waiting;若其中一个任务是 monitor,waitingFor 为 monitor,否则为 shell。这些任务是从 updates.jsonl 中唯一读取的内容:其 _x.ai/session/update 行中的 background_tasks 与 task_completed;其他所有行均会跳过而不解析。未知阶段不会设置 status。标题和 model 来自 summary.json,对话来自 chat_history.jsonl,子 Agent 来自 subagents/<id>/meta.json。绝不会打开 active_sessions.lock、*.tmp、auth.json 和会话搜索 sqlite。
Provider ID 为 codex-cli,harness 为 Codex。Home 目录在设置 $CODEX_HOME 时为该路径,否则为 ~/.codex,可通过 codexCli({ home }) 覆盖。与该 Home 目录共享的 VS Code Codex 会话属于同一个 Provider。
实时会话是进程当前打开的 rollout 文件:<home>/sessions/YYYY/MM/DD/rollout-<timestamp>-<thread-id>.jsonl。没有 pid 索引。状态来自持久化的轮次生命周期事件:task_started → running;task_complete / turn_aborted → idle。Codex 不会持久化审批,也没有能唤醒已结束轮次的后台工作(存活时间超过轮次的统一 exec 进程不会被记录为运行中),因此永远不会报告 waiting。标题先来自 session_index.jsonl,再回退为第一个用户提示词;对话来自 response_item 记录;子 Agent 来自带有 parent_thread_id 的子 rollout。压缩的 .jsonl.zst 线程仅当 session_index.jsonl 中包含其标题时才会列出,因此压缩子线程不会被视为会话(未命名的压缩根线程会被省略)。绝不会打开 auth.json、state_*.sqlite、ipc/、process_manager/chat_processes.json 和 history.jsonl。
直到第一个提示词写入 rollout 前,会话都不存在。watch 启动后恢复的会话会在首次追加时出现。安静卸载的 VS Code 线程会一直列出,直到该 pid 的下一个事件发生。
Provider ID 为 oh-my-pi,harness 为 OhMyPi。Home 目录为 ~/.omp(或 ~/$PI_CONFIG_DIR),可通过 ohMyPi({ home }) 覆盖。PI_CODING_AGENT_DIR 会被遵循;一旦 $XDG_DATA_HOME/omp / $XDG_STATE_HOME/omp 存在,也会遵循这些 XDG 位置,与 omp 自身行为一致。<home>/profiles/ 下,或 $XDG_DATA_HOME/omp/profiles / $XDG_STATE_HOME/omp/profiles 下的每个具名 profile 也都会被观察;删除 profile 会关闭其会话。不支持上游 pi(~/.pi)。
omp 的注册表分为两部分。每个进程启动时会写入 run/daemons/<project-hash>/clients/<pid>-<uuid>.json,并在正常退出时删除。每个连接终端的进程会写入 agent/terminal-sessions/<tty>,其中标示其当前使用的会话文件;切换会话(/new、/resume)时会重写该文件,并会在目标文件尚不存在时标记为 fresh。两部分通过进程的控制终端关联,每个进程通过 processes.tty(pid) 读取一次。终端上早于该进程的 breadcrumb 不属于该进程,也不会绑定任何内容。
转录记录:agent/sessions/<encoded-cwd>/<timestamp>_<id>.jsonl。目录名称是有损的,因此 cwd 来自文件头。保存在其他位置的会话(--session notes/work、--session-dir)通过 agent/custom-session-files/ 查找,并会在其文件存在后通过 breadcrumb 绑定;omp 不会为文件名不是 *.jsonl 的文件提供子 Agent。状态来自最后一条对话记录:用户消息、工具结果,或 stopReason 为 toolUse 的 assistant 消息 → running;stop / length → idle;error → idle,且该轮次为 failed 并带有其 errorMessage;abort
English / 原文
Watch every coding agent on this machine, and inspect the sessions they leave behind.
Subagents covers start, end, background and nested.
claude -pwrites no live index entry, so a print-mode run appears in history only.- Grok's
tool_startedcarries no call id, so the current tool is identified by name. Two tools with the same name running at once show as one. - A failed turn is reported as
failed, but Grok records no error message for it, soactivity.erroris usually empty. grok -pregisters as live only whenGROK_TRACK_HEADLESSis set. Otherwise it appears in history only.- 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/resumeafter 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. - 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
sqlitereader. Without it a new session readsidleuntil its transcript appears. - A pending
asktool readswaiting. Permission approvals never reach disk, so a session blocked on one readsrunning(omp's default approval mode asks for none). - omp names a run after the terminal on its stdin, in print mode too:
omp -ptyped 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 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.
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 Turns; events() tails normalized SessionEvents 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 |
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.
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.