All posts
Nov 7, 2026

Sessions Are Already Agents

Last week I moved my loops out of herdr. The sessions that stayed behind had turned into a team of specialists, with me carrying messages between them. So I built the way out: adopting sessions into automator, a message bus between them, and Jev deciding who needs to hear a broadcast.

Ben Cao · 9 min read · 0 comments

Last week I moved my hourly loops out of herdr and into automator, and herdr went back to holding the sessions I'm actually working in. With the loops gone, I could see something about those sessions that I'd missed: I had stopped closing them.

herdr is a terminal multiplexer built for coding agents. Each workspace holds a session, and the sidebar shows which ones are working, which are idle, how full each context is and what each one has cost. Since keeping a session alive cost nothing, I stopped throwing them away. Today I have about fifteen open. One has spent a week on a data pipeline. One knows a new module's state machine better than I do. One has been through every revision of a design doc.

Last week's post ended with me managing a small team of loops. It turns out I had a second team in herdr, made up of specialists, and nobody planned it.

The context is the asset

A session that has worked on one problem for days holds things that exist nowhere else: which approaches failed, what the code looked like before the refactor, why a constraint is there, and which test is flaky and why. Some of that makes it into commits and docs, but a lot of it doesn't.

Starting a fresh session means paying for all of that again: re-reading files, re-deriving decisions, and sometimes getting them wrong in a different way. The long-running session already has it in context. So when I had a question about the data pipeline, the cheapest correct answer was no longer "go read the code". It was "ask the session that wrote it."

I became the router

So I started doing exactly that. I'd type something like this into one session:

Before you change the schema, ask the pipeline session how it handles late-arriving rows. It's in the tab next door.

Then I'd go to the tab next door, paste the question, wait, copy the answer, come back and paste it in. Sometimes the second session had a question for the first, and I'd carry that back too.

It worked, and the answers were better than what either session would have produced alone. But look at what I was doing: moving text between two processes that sat a few centimetres apart on my screen. I was the message bus, and a slow, lossy one that forgot things and went to lunch.

Updates were worse than questions. When one session changed an API, the three sessions that called it found out only if I remembered to tell them, and usually I didn't.

automator had the same problem. loop-doctor, the loop that reviews other loops, proposes changes that I then apply by hand. That's me as the bus again, just with a nicer interface.

No framework required

It's easy to talk about "agents" as something you build with a framework: an orchestrator, a planner, roles and a graph. I think that gets it backwards.

A Claude Code session already has a goal, memory, tools and judgment. It reads, decides, acts and reports. By any reasonable definition it is an agent. The only reason it doesn't behave like one in a team is that everything goes through a human: it runs only when I type, and it talks only to me.

So the question isn't how to build agents. It's what to remove so the agents I already have can work together. The answer is me, at least in the parts where I add nothing.

Two things were missing, and on Monday I built both.

1. Adoption: running a session when nobody is typing

automator already did this for loops. Each loop owns one continuous session: the first run creates it and every later run resumes it, so the loop remembers what it did last time. When a loop needs me, it asks a question and ends its turn, and my answer becomes its next message.

But loops started as loops. Adoption takes a session that grew up interactively in a herdr pane, with all its context, and hands it to automator. Nothing is copied or summarised. It's the same session id, so the same transcript and the same memory, with a new owner.

Half of this already existed in the other direction: last week's automator could continue or fork a loop's session in my terminal. Adoption is the same handoff run backwards, and it has the same custody rule. An interactive claude process and a background claude --resume can't share a session. Both would append to the same transcript, and the interactive one never reloads, so it would carry on from a history that no longer matches.

So adoption is a clean handoff. automator lists the herdr panes running Claude Code, and for each one it either offers to adopt it or says why it can't (it's mid-turn, it's already a loop, or its directory is gone). When I adopt one, the pane's claude exits and automator takes over. From then on:

  • It runs where it grew up. The loop works in my own checkout, never cleans it up, and is blocked from switching branches under me.
  • It waits for me. When I want to talk to it, I continue it in my terminal, and every kind of run waits until I close it. Give back turns the loop off and reopens the session in a pane.
  • It doesn't keep the old bill. What the session spent before adoption is shown separately, so a loop's budget covers only its own runs.

The first thing an adopted session does is set itself up. If its context is too full, it compacts first. Then it writes a profile, a short note on what it knows and what it's working on, so its peers know when to ask it. Last, it proposes its own goal and schedule. Last week's main lesson was that a loop prompt is a standing job description. An adopted session already knows its job, so it writes the description itself, and I approve or edit it in Needs you like any other proposal. By default it has no schedule. It runs when someone sends it a message.

2. The bus: sessions reaching each other

Once the sessions are loops, messaging is plumbing. A message to a loop is just a queued input that starts its next run, so it waits behind the daily budget and the shared checkout like any other trigger. On top of that:

  • Directory. A loop can list its peers with their goals and profiles.
  • Ask, reply and tell. A loop can ask a peer a question or send it a note. Nothing blocks: the asker carries on or ends its turn, and the reply arrives as one of its later messages, tagged with the question it answers.
  • Broadcast. A loop can announce something, such as "the old endpoint stops working on Friday", without knowing who cares.

Broadcast is where it gets interesting

Sending every broadcast to everyone is the naive version, and it fails quickly. Last week taught me what costs money: reading a big context on every run. Three loops made up 84% of the week's spend, simply because they woke up hourly and reread everything. An adopted session with a week of context is the most expensive thing in the system to wake. With 22 loops and fifteen sessions, each broadcasting a few times a day, the naive version wakes those sessions over and over to read announcements that don't concern them.

Hand-written subscriptions (topics, tags, routing rules) are the other obvious answer, and they go stale the day you write them. A session's interests change as its work changes.

What I want instead is a cheap, fast judgment for each event and each candidate: would this loop act differently, or need to know, given this event? That is common-sense classification, not open-ended reasoning, so it doesn't need a full LLM call to answer it.

For this I use Jev, a model built for this kind of typed judgment. You give it a structured state and a yes/no question with criteria, and it returns a probability. The same day I started using it for a related job too: judging the tool calls a loop's block list denies, so the safe ones don't have to wait for me.

For the bus, the state is the broadcast plus the candidate's goal, profile and current status, with one Jev request per candidate. Before picking a threshold I hand-labelled 133 broadcast-and-loop pairs as relevant or not, and probed Jev with them. At a threshold of 0.7, the bus delivers 90% of the broadcasts that matter, and 78% of what it delivers is relevant. That's the default setting, called Balanced. One request per candidate and one batched request for all of them agreed on 128 of 140 judgments, so I kept the simpler version.

Anything below the threshold is dropped and logged, and the receiving loop is never woken. If a delivered broadcast turns out not to matter, the receiver says so, and Jev reads that loop's last five "not for me" notes the next time it judges.

If Jev can't answer, the broadcast is dropped rather than delivered. That's the opposite of how I handle tool approvals, where a failure means asking me. Here a wrong delivery wakes an expensive session for nothing. A missed delivery can matter too, which is why every decision goes to a log I can read.

The parts that scare me, and the guardrails

Removing the human from the loop sounds reckless, so here is what "removing" actually means.

  • I'm only removed as the router. Loops can't approve each other's tool calls, answer each other's questions to me, or change each other's settings. The loops that supervise other loops stay off the bus entirely. Anything that already needed me still needs me.
  • Hop limits. A message sent from a run that a peer message started counts as one hop further, and past two hops it's refused. A asks B, B can ask C, and C can't ask anyone else. Replies carry their question's hop count, so every answer still finds its way back. A pair of loops also gets at most six messages an hour, and each run can send only a few of each kind, so two sessions can't talk each other into an infinite conversation.
  • Taint travels. Some loops read text written by other people, such as Slack messages and PR comments. Last week I listed that as one of the things I'm watching. Anything such a loop sends is marked as tainted, and when the receiver runs on it, Jev treats its tool calls as coming from a loop that read untrusted text. A tainted run also can't rewrite its own profile, because profiles reach other loops. Otherwise a stranger's message could be laundered through a trusted loop into an action nobody approved.
  • The receiver pays. A woken run counts against the receiving loop's own daily budget. A loop that has used up its budget isn't a broadcast candidate, and messages to it wait. A chatty peer can't spend someone else's budget.
  • Everything is logged. A Bus page lists every message and every broadcast decision, including Jev's score and why something was dropped, with links to the run that sent it.

Where this is

Adoption, the bus and the Jev filter were all built on Monday, with tests, and I checked adoption end to end: an adopted session kept its context, wrote its profile and proposed its own setup. What I don't have yet is mileage. No real session has asked another one a question yet.

So the numbers above are from a probe, not from use. I'll write a follow-up once the bus has run for a few weeks, with real numbers: how many broadcasts Jev filters out, how often it's wrong in practice, what adopted sessions cost to keep around, and whether sessions asking each other questions actually beats me doing it.

My guess is that it will. Even on a good day I'm a slow, forgetful message bus.


If you enjoyed it, you can buy me a coffee ☕.

No comments yet. Be the first.

Optional. Leave it blank to post as Anonymous.