<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
	<channel>
		<title>AgentBar Blog — English</title>
		<link>https://agentbar.pages.dev/blog</link>
		<description>Building AgentBar, a read-only macOS menu bar monitor for AI coding agents: measurement without process spawns, trace reconstruction, and privacy by design.</description>
		<language>en</language>
		<atom:link href="https://agentbar.pages.dev/feed.xml" rel="self" type="application/rss+xml" />
		<item>
			<title>Why AgentBar reads files instead of spawning your agents</title>
			<link>https://agentbar.pages.dev/blog/read-only-by-design</link>
			<guid isPermaLink="true">https://agentbar.pages.dev/blog/read-only-by-design</guid>
			<pubDate>Sat, 01 Aug 2026 00:00:00 GMT</pubDate>
			<description>CodexBar froze a Mac every minute because it spawned Claude CLI to read usage. AgentBar reads the files agents already write, and tests prove it spawns nothing.</description>
			<content:encoded><![CDATA[<p>Last July my Mac started freezing for nearly a minute, once a minute. The pointer moved, stopped, then caught up in a rush; keystrokes queued and arrived late. I assumed a runaway Chrome tab had finally won. It hadn&#39;t. The process tree pointed at a menu bar app I had installed to keep an eye on my coding agents, and the thing it kept starting, sixty seconds apart, was the agent itself.</p>
<p>The app was CodexBar 0.43.0, Apple-notarized, with its refresh interval set to one minute. To show me a usage number, it spawned the Claude CLI. The CLI did not run in a clean sandbox. It read my ordinary Claude configuration, which meant every probe also initialized my two MCP servers (Context7 and Serena) and my Chrome integration. Chrome arrived with its GPU process, network service, storage service, and a swarm of renderers restoring tabs and extensions.</p>
<p>The measurements from that afternoon are in the repository, with timestamps. Two probes started at 14:56:48 and 14:57:48, one minute apart, both children of CodexBar&#39;s watchdog. Renderers individually took 30 to 111 percent of a core. Free memory fell from roughly 220,000 pages to about 4,000; compressed memory went from zero to over 100,000 pages; WindowServer climbed to 33–40 percent CPU, and input stopped reaching the screen. I killed CodexBar at 14:58:42 and watched for two more minutes. No new probes appeared.</p>
<p>To be fair to CodexBar, it was not the only load on that machine. Two USB display tools (DisplayLink and InstantView) were also running, and they amplify anything that stresses WindowServer. But the periodic trigger, the thing that turned background weight into a frozen desktop every sixty seconds, was a menu bar app reading usage the expensive way.</p>
<h2 id="reading-usage-should-not-cost-a-cli">Reading usage should not cost a CLI</h2>
<p>I went through CodexBar&#39;s GitHub issues afterwards, about two hundred of them, and the same design shows up everywhere. When your data source is &quot;ask the CLI,&quot; and the CLI is a full agent harness, every refresh carries the weight of your whole setup. The more MCP servers and integrations you configure, the heavier the menu bar update becomes.</p>
<p>The issue tracker makes the pattern concrete:</p>
<ul>
<li><a href="https://github.com/steipete/CodexBar/issues/1844">#1844</a>: an hourly &quot;delegated CLI refresh&quot; recovery path called <code>/usr/bin/open</code> and launched the user&#39;s default browser, in a fast retry loop every 20 seconds to 3 minutes.</li>
<li><a href="https://github.com/steipete/CodexBar/issues/2052">#2052</a>: background Claude probes triggered CLI auto-update downloads because <code>DISABLE_AUTOUPDATER=1</code> was never set. Ninety gigabytes of traffic in three days, and 257 to 495 Claude CLI processes spawned per day.</li>
<li><a href="https://github.com/steipete/CodexBar/issues/2251">#2251</a>: every usage probe minted a new empty Claude session. Thirty-six phantom sessions accumulated in the account within weeks, and the probes started tripping Anthropic&#39;s rate limits.</li>
<li><a href="https://github.com/steipete/CodexBar/issues/2241">#2241</a>: the CLI probe succeeded 23 times out of 268, an 8.6 percent hit rate. On failure it threw away the cached snapshot, leaving gaps of up to 110 minutes in the displayed data.</li>
<li><a href="https://github.com/steipete/CodexBar/issues/1999">#1999</a>: two CodexBarCLI processes grew to 75 GB combined on a 48 GB machine and took the whole system down with a jetsam OOM.</li>
</ul>
<p>None of these are carelessness. They are what happens when a passive monitor keeps booting an active agent, one probe at a time.</p>
<h2 id="the-files-were-already-there">The files were already there</h2>
<p>The alternative was sitting on disk the whole time. Coding agents record their own work. Claude Code writes <code>~/.claude/projects/**/*.jsonl</code>. Codex writes <code>~/.codex/sessions/YYYY/MM/DD/*.jsonl</code>. Qwen writes chat JSONL plus a <code>usage_record.jsonl</code>. Gemini writes event-sourced session JSONL. Cursor keeps its state in a SQLite database (<code>state.vscdb</code>), which AgentBar reads directly through the sqlite3 C API instead of asking Cursor for anything.</p>
<p>Everything AgentBar shows comes from those files. Daily token usage, quotas, per-model breakdowns, sixty days of session history, the graph of <a href="/blog/reconstructing-agent-traces">which agent called which</a>, even the &quot;waiting for your approval&quot; state. That last one is not a heuristic. For Claude, it is read from the tail of the raw JSONL: an unfinished <code>tool_use</code> record with no matching result means the agent is blocked on a human, and AgentBar pins those rows to the top.</p>
<p>Claude&#39;s quota numbers come from <code>~/.claude/usage-watch/rate-limits.json</code>, a file the Claude CLI itself writes after each exchange. AgentBar never asks a server and never starts a process to get them.</p>
<h2 id="enforced-not-promised">Enforced, not promised</h2>
<p>&quot;Read-only&quot; is easy to claim and hard to keep, so AgentBar does not rely on discipline. Two mechanisms make it structural.</p>
<p>At runtime, <code>ProcessGuard</code> diffs the system process table with <code>sysctl</code> around every fetch. If a refresh ever spawns a forbidden child (Node, Python, npm, uv, a browser, an MCP server), the monitor notices its own transgression and reports it. In the test suite, <code>NoProcessSpawnTests</code> goes further: it registers ten MCP servers into a Claude configuration, runs a hundred refresh cycles against it, and fails the build if a single Node, Python, npm, uv, Chrome, or MCP process appears. The test is the receipt.</p>
<p>The rest of the design follows from the incident list above. A self-monitor measures AgentBar&#39;s own CPU and RSS with <code>getrusage</code> and <code>task_info</code>, and stops automatic refreshes if it ever crosses a threshold. Failed fetches do not blank the UI; the last good value stays on screen with its timestamp, stale-while-revalidate, which is the direct opposite of issue #2241. The shortest refresh interval on offer is five minutes. There is no one-minute option, by design. Refreshes are single-flight per provider, staggered seconds apart, and a provider that keeps failing gets its circuit breaker opened for fifteen minutes.</p>
<p>The only place AgentBar spawns subprocesses at all is the tool updater, on demand, with hard timeouts, and only when you click it. The Diag tab shows the books: last fetch duration, CPU time, peak RSS, files read, records parsed, and forbidden processes spawned. That last counter is always zero.</p>
<h2 id="what-you-give-up">What you give up</h2>
<p>Reading files has a cost, and it is honesty about that cost that separates a monitor from an ad.</p>
<p>File reading sees what the agent has already flushed to disk, not live API truth. Quota numbers are exactly as fresh as the file that carries them. Run-state detection is precise for Claude, whose log tails make the state explicit, and best-effort for Codex, Qwen, Gemini, and Cursor, where it is inferred from conversation turns. Cost figures are estimates against a model price table, and they may well be covered by a subscription; the UI says &quot;estimated&quot; every time. Cached tokens are priced separately from generated ones so estimates do not double-bill, which is the bug behind CodexBar&#39;s issue <a href="https://github.com/steipete/CodexBar/issues/1796">#1796</a>, where costs ran 2.7 to 4.6 times too high.</p>
<p>And an agent that keeps no local records cannot be watched this way, full stop. For the rare case where a provider&#39;s data only exists remotely, AgentBar has a remote usage query, but it is opt-in, configured per provider, and the single exception to the read-only rule. Nothing leaves the machine by default.</p>
<h2 id="the-quiet-part">The quiet part</h2>
<p>The fix that afternoon was quitting an app. The reason to write a new one was the realization that &quot;monitor&quot; and &quot;spawn&quot; had no business being the same verb. A usage meter should get heavier when your agents work harder, never when you configure more tools.</p>
<p>My Mac has been quiet since, which is an observation about my Mac, not a promise about yours. The promise lives in the repository: in <code>ProcessGuard</code>, in <code>NoProcessSpawnTests</code>, and in a Diag tab that prints the receipt on demand.</p>
]]></content:encoded>
		</item>
		<item>
			<title>How AgentBar reconstructs multi-agent traces from local logs</title>
			<link>https://agentbar.pages.dev/blog/reconstructing-agent-traces</link>
			<guid isPermaLink="true">https://agentbar.pages.dev/blog/reconstructing-agent-traces</guid>
			<pubDate>Sat, 01 Aug 2026 00:00:00 GMT</pubDate>
			<description>Three coding agents, one request, one calling the next. AgentBar rebuilds the call graph and state from files the agents already write, and labels every edge it infers.</description>
			<content:encoded><![CDATA[<p>You ask Claude Code to refactor a module. Halfway through it decides the job needs another agent and runs <code>codex exec</code> in a shell. That Codex, in turn, calls <code>claude -p</code> to check a diff. One request from you, three agents, two process boundaries crossed without your involvement.</p>
<p>Later you want the whole chain. Who called whom, with what command, and what each one is doing right now. In a service backend this is what distributed tracing is for. A trace context propagates across each hop, a collector assembles the spans, and a UI draws the waterfall. The catch is the instrumentation. Somebody has to inject the context and run the collector.</p>
<p>You installed none of that. No hooks in the agents, no tracer library, no daemon watching process creation. So the fair question is how a trace can exist at all.</p>
<p>The short answer is that it does not exist, in the tracing sense. Nothing was captured at the boundary. AgentBar reconstructs the graph afterwards, from records the agents wrote for their own reasons, and it marks every connection that required a guess.</p>
<h2 id="the-records-were-already-there">The records were already there</h2>
<p>Coding agents keep transcripts. Claude Code writes <code>~/.claude/projects/**/*.jsonl</code>. Codex writes dated session JSONL under <code>~/.codex/sessions/</code>. Qwen and Gemini keep their own JSONL. Cursor stores state in a SQLite database, which AgentBar reads directly through the sqlite3 C API rather than asking Cursor anything.</p>
<p>These files exist so the agent can resume a session and show you history. They are a side effect of the agent working, not of the agent being watched. AgentBar reads them and nothing else. It spawns no CLI, installs no hook, injects no tracer. The companion piece on <a href="/blog/read-only-by-design">reading files instead of spawning agents</a> covers why that constraint matters. Here the point is narrower: the raw material for a trace was on disk the whole time.</p>
<p>A trace built this way is forensic. It works on what already happened, from artifacts left behind. Live telemetry reports truth as it crosses a boundary. Reconstruction reads the receipts and infers what most likely happened.</p>
<h2 id="finding-the-moment-one-agent-calls-another">Finding the moment one agent calls another</h2>
<p>The first job is to notice a crossing at all. AgentBar&#39;s <code>EdgeDetector</code> scans the parent run&#39;s conversation turns, looking only at tool and assistant turns, since user and system turns do not execute commands. Within those turns it splits each line at the shell separators (<code>&amp;&amp;</code>, <code>;</code>, <code>|</code>, a lone <code>&amp;</code>) and asks what each segment actually runs.</p>
<p>The tokenizer is deliberately shallow, and the source says so. It skips leading environment assignments (<code>FOO=bar</code>) and command wrappers (<code>nohup</code>, <code>sudo</code>, <code>env</code>, <code>time</code>, and friends), then takes the first real token and strips any path. <code>nohup codex exec &quot;…&quot;</code> reads as <code>codex</code>. <code>/usr/local/bin/claude -p &quot;…&quot;</code> reads as <code>claude</code>. Agents launched detached or under sudo are exactly the ones worth tracking, so the wrappers matter.</p>
<p>This is no shell parser, and the job does not require one. The task is to tell a launch command apart from prose. In <code>codex exec &quot;fix the tests&quot;</code>, the agent name is the command token. In a sentence like &quot;I asked claude to review the diff&quot;, it never is, so the sentence produces nothing. That single distinction removes most false positives before correlation even runs.</p>
<p>Each detection becomes an invocation: the command that crossed the boundary, the target agent, the timestamp of the turn that contained it, the parent&#39;s working directory, and the parent tool-call id when the command came from a tool result.</p>
<h2 id="turning-an-invocation-into-an-edge">Turning an invocation into an edge</h2>
<p>An invocation says the parent announced another agent. On its own it does not say which session that agent started. The binary could have run and exited inside the same turn, or the matching session might be one of several. AgentBar correlates.</p>
<p>For each invocation it filters the candidate sessions. A session qualifies when its provider matches the target agent, it started after the invocation and within a 60-second window, and, when both sides carry a directory, the project equals the invocation&#39;s working directory. A missing directory on either side is treated as unknown rather than as a mismatch. If several sessions qualify, the earliest start wins.</p>
<p>The result is an edge, and the data model is explicit about what kind. Every edge this path produces carries an <code>.inferred</code> confidence and renders as a dashed line. The model also defines a <code>confirmed</code> confidence, for a linkage passed across the boundary by an environment variable or session id. The file-only path never emits one. There is no cross-process signal to confirm against, only timing and directory coincidence, so AgentBar does not claim certainty it does not have.</p>
<p>This is the honesty the format allows. A solid line would mean the linkage was observed. A dashed line means AgentBar saw a launch command, saw a matching session appear shortly after in the same place, and concluded one caused the other. Usually that conclusion is right. The label exists for the times it is not.</p>
<h2 id="what-the-graph-draws">What the graph draws</h2>
<p>The Graph window opens from the Active tab in the menu bar and lays one human request out left to right, in the style of an X-Ray or Datadog trace map. Shape carries identity and color carries state, so the two never compete.</p>
<p>Circles are actor points: a human prompt, an agent run, a terminal marker. Squares are tool calls. Triangles are model thought, system notes, and approval waits. An agent run draws as a circle with a provider-specific border and a 6-point status dot, colored by one of five states: running, waiting for approval, completed, failed, or idle.</p>
<p>The node is a run, not a session. A session can contain many runs, each <code>--resume</code> or re-run of a thread, and parent-child edges are keyed to the run. Resuming a conversation does not orphan its children.</p>
<p>A child agent expands inline at the point where it launched. When <code>bash</code> runs <code>codex exec …</code>, that Codex draws as a child lane rather than a plain tool call. The parent lane descends into the child flow and merges back once it completes, and deeper nesting means deeper lanes. Consecutive thoughts of the same kind collapse into a single <code>×N</code> node; clicking it opens every merged message, and clicking any shape opens an inspector with the thought content, the tool command and output, or, for an agent run, the model, tokens, cache hit rate, changed files, and every event. Large graphs stay navigable through two-direction scrolling, zoom from 35 to 200 percent, and a fit button.</p>
<h2 id="state-is-a-function-of-the-log-tail">State is a function of the log tail</h2>
<p>Knowing who called whom is half the question. The other half is what each agent is doing now. AgentBar answers it the same way it builds the graph, by reading files and computing.</p>
<p>Each provider reduces its log tail to a small signal: the timestamp of the last record, the kind of the last event, whether a tool proposal sits unanswered, whether a terminal success or failure appeared. A pure classifier turns that signal into a state with a fixed priority. Failure wins first, regardless of recency. A fresh log with an unanswered tool proposal reads as waiting for approval. A fresh log with an open question reads as waiting for input. Fresh and still producing reads as running. A terminal stop that has gone cold reads as completed. Anything else is idle.</p>
<p>The freshness window matters. An approval wait only makes sense while the log is recent, so once a log goes stale the classifier stops claiming the user owes the agent anything.</p>
<p>Precision varies by provider, and the UI says so. For Claude the approval wait is read from the raw JSONL tail: an unfinished <code>tool_use</code> with no matching result record. For Codex, Qwen, Gemini, and Cursor it is a best effort inferred from conversation turns, and the state note carries the hedge. The menu bar&#39;s Active tab polls the session directories on a 10-second throttle, limits the live graph to the last three days and 25 runs per provider, reuses unchanged summaries and conversation tails from an in-memory cache, and pins anything waiting for approval to the top.</p>
<h2 id="what-reconstruction-gives-up">What reconstruction gives up</h2>
<p>Reading receipts has a cost, and naming it is what separates a monitor from a demo.</p>
<p>Correlation can misfire. Two Codex sessions started in the same directory within a minute of the same invocation both look like candidates, and the earliest wins even if the other was the real child. The 60-second window and the directory check keep this rare rather than impossible. A crashed agent and one waiting for approval can leave similar log tails, which is why the state carries a note instead of a guarantee. Gemini is detected as a launch command but never correlates to a session, because AgentBar has no Gemini provider to match against yet. The invocation is recorded; the edge is not.</p>
<p>And the whole picture is only as current as the files that feed it. AgentBar sees what the agents have flushed to disk, not live process truth. A run that finished a second ago but has not written its terminal record still looks active.</p>
<p>None of this is hidden in the rendering. Inferred edges are dashed. Estimated states carry a note. The Analysis pane, a set of pure functions over the graph, reports per-provider tokens, estimated cost, cache hit rate, and run count, alongside graph health: maximum depth with a warning for chains stacked too deep, orphan runs, and edge count, with the longest run named as the bottleneck. It describes what it sees and flags where it guessed.</p>
<p>The trace was never collected. It was left behind, one JSONL line at a time, by agents that had no idea anyone would assemble their receipts into a graph. AgentBar&#39;s job is to read those receipts carefully, connect them only where the evidence supports a connection, and say plainly where it had to guess. The causal chain you wanted is on your disk. So is the honesty about how much of it is inference.</p>
]]></content:encoded>
		</item>
	</channel>
</rss>
