Skip to content

refactor(agents): separate the agent runner and its reads from the desktop app - #961

Merged
loganj merged 7 commits into
mainfrom
agent-host-runner
Oct 14, 2026
Merged

loganj merged 7 commits into
mainfrom
agent-host-runner

Conversation

@loganj

@loganj loganj commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

🤖

Summary

  • First of three PRs that let a Buzz agent run without the desktop app open (see Why).
  • The runner. The code that decides when an agent runs was mixed into the app's agents service, together with the app's own connection, storage and screens. This PR moves that code into a separate module, so a program outside the app can use it unchanged.
  • The reads. Before an agent answers, the Claude and Codex code reads the channel, the recent conversation, people's names and the agent's saved memory. Today it gets all of this from the owner's signed-in session inside the app, which a program outside the app does not have. Now this code asks for those reads through one small interface. The app fills the interface from the owner's session, exactly as before. The standalone program in feat(agent-host): run a saved agent without the desktop app #967 fills it by reading as the agent.
  • No behavior change in the app. The existing agents service tests pass without edits.

Why

Buzz agents are AI assistants, such as Claude or Codex, that a person sets up in the Buzz desktop app. They answer when someone mentions them in a channel, and they can also watch for events and run on a timer. Today an agent runs only while its owner's desktop app is open, because the code that runs agents lives inside the app.

The goal of this stack is a small command-line program that runs an agent with the app closed. It must use the same code as the app, so the agent behaves the same wherever it runs. The app keeps working exactly as it does today.

Stack

Review in order. Each PR keeps the app working.

  1. refactor(agents): separate the agent runner and its reads from the desktop app #961 (this PR): move the code that decides when an agent runs into its own module, and let the Claude and Codex agent code read conversations without depending on the app.
  2. feat(agents): keep agent state in files that a program outside the app can read #974: keep agent state in files that a program outside the app can read.
  3. feat(agent-host): run a saved agent without the desktop app #967: add buzz-agent-host, the program that runs an agent with the app closed.

Details

The runner (refactor(agents): move the agent runner out of AgentsService)

  • New src/features/agents/runner.ts (AgentRunners). It owns the rules for running an agent:
    • one queue of work per agent, and a rate limit;
    • matching mentions and watched events to the agent;
    • running timers when they are due;
    • a time limit for each run;
    • the check that only one copy of the app runs a given agent (the app already uses a lock file for this).
    • holding an idle agent while its type changes (fix(agents): manage providers, models, and agent migrations #952), so no run starts during the change.
  • The runner gets everything else from the program that hosts it, through one interface, RunnerHost. That includes which agents are ready, their saved settings, team prompts, channel membership, the activity view, the typing indicator, and the native key store, which signs the agent's messages and makes its relay requests.
  • AgentsService implements RunnerHost with the same code it used before. It keeps connections, the agent list, hosted agents and settings edits.

The reads (the claude-code and codex commits)

  • src/features/agents/reads.ts: the AgentReads interface (channel facts, conversation reads, profiles, names, memories, and which community the agent is in), plus ownerReads, the app's implementation. ownerReads is the existing code, moved.
  • Before, these runtimes got the owner's whole session object from the plugin API (ctx.agents.relay(pubkey)). That object exists only inside the app. The interface lists only the reads an agent needs.
  • src/bundled/claude-code/run.ts and src/bundled/codex/run.ts: the parts of each agent type that run an agent (model list, run, hand-over deadline), without its settings screens. A program with no user interface can load them. The app registers them with their screens as before.
  • The memory read now accepts a cancel signal. Claude keeps its existing 5-second limit.
  • fromOwner, the check that decides who can give an agent instructions, moves next to the signature check it uses. The agents service still exports it. It also accepts mentions from relay workflows (fix(agents): accept verified relay workflow mentions #970). That part needs the community's workflow key and signed profiles, so it now gets them through the same interface (workflowOf).
  • The lists of saved Claude and Codex sessions (feat(agents2): show Codex and Claude Code session transcripts #842) get channel names through the same interface.
  • The Claude and Codex runtime tests now wrap their fake session in ownerReads. The Codex tests also add one regression test: an agent stopped while its channel is read starts no work. One assertion now names the "foreground" profile priority. That is already the default, so behavior does not change.

@loganj loganj changed the title refactor(agents): move the agent runner out of AgentsService refactor(agents): move the code that runs agents out of the agents service Oct 13, 2026
@loganj loganj changed the title refactor(agents): move the code that runs agents out of the agents service refactor(agents): separate the agent runner and its reads from the desktop app Oct 13, 2026
@loganj
loganj force-pushed the agent-host-runner branch from d6928af to 7ce397b Compare October 13, 2026 17:37
@loganj
loganj marked this pull request as ready for review October 13, 2026 19:48
@loganj
loganj requested a review from a team as a code owner October 13, 2026 19:48

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two changes requested: preserve cancellation across the new asynchronous channel lookup, and migrate the documented Codex live-check caller to the new reads interface. Details are inline.

Star Lord's automated source review via Wes's account. Head: 7ce397b7ba1c924ac544318335c366338d64ba57; base: 96f1c2155eb0406349cecba5575bb01be0942ce8. Source-only; no tests or app/native execution. CI snapshot: required checks passed; Windows native validation skipped. Live runtime behavior remains unverified.

Comment thread src/bundled/codex/runtime.ts
Comment thread src/bundled/codex/runtime.ts

@wpfleger96 wpfleger96 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 thanks for splitting this out, the runner and reads seams read cleanly.

one blocker: this doesn't type-check once it's merged with current main. #846 landed today and added new this.relay(...) callers: channelName() in claude-code/runtime.ts (used by transcript discovery in find()), and name() plus find() in codex/runtime.ts. this PR removes the relay field, so git merges cleanly but both JavaScript CI jobs fail pnpm typecheck with TS2339: Property 'relay' does not exist (run). typecheck fails before the vitest shards start, so the unit suites haven't run on the merge either. could you rebase onto main and move those three onto AgentReads.channelName() / AgentReads.name(), keeping how they resolve names today? once that's done it'd be worth checking that saved-transcript discovery and display still work for both runtimes.

nonblocking: the description says the Codex runtime tests change "only how they build their fake session". they also add the stop-during-channel-read regression test and tighten the profiles.ensure assertion to "foreground". that's not a behavior change, since foreground is already the default, but the sentence should match.

I diffed the code removed from service.ts against runner.ts, and the move is faithful: this.X becomes this.host.X, the origin check becomes the heard predicate, and the order of reconcile, notify and resume is unchanged. fromOwner is byte-identical, and workflowOf keeps the same authority and profile revalidation. The Codex cancellation and live.mjs fixes from the earlier review are in.

Comment thread src/bundled/claude-code/runtime.ts
Comment thread src/features/agents/reads.ts Outdated
Comment thread src/features/agents/reads.ts Outdated
Comment thread src/features/agents/runner.ts
@loganj
loganj force-pushed the agent-host-runner branch 4 times, most recently from a4dd702 to a839030 Compare October 13, 2026 21:52
wpfleger96
wpfleger96 previously approved these changes Oct 13, 2026

@wpfleger96 wpfleger96 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 rechecked at a839030 and the blocker's fixed. the #846 reads go through AgentReads now, members is optional so Codex name() resolves the same way main does, and profiles returns Promise<void>. the rebase over #949 keeps the wait for the first Claude/Codex download, it just moved into claudeRun/codexRun. keying the Codex memory read by connection in a839030 is a good catch too, the move had lost that.

nonblocking: Codex name()/find() don't have a test through the reads like the Claude one in 98d993b, fine as a follow-up. adding the fake-host runner test in #967 works for me.

@wpfleger96
wpfleger96 dismissed their stale review October 13, 2026 22:25

🤖 holding this until a second review finishes, will re-approve once it's clear

@wpfleger96 wpfleger96 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 second review came back clear too, so approving again at a839030. same notes as my earlier approval: the #846 reads go through AgentReads, the #949 download wait carries over into claudeRun/codexRun, and the connection-keyed Codex memory read looks right.

nonblocking: Codex name()/find() still don't have a test through the reads like the Claude one in 98d993b, fine as a follow-up.

@loganj
loganj force-pushed the agent-host-runner branch 3 times, most recently from ba2a08e to dd1c24f Compare October 14, 2026 13:10
loganj and others added 7 commits October 14, 2026 10:00
The per-agent queue, rate limit, mention and watch matching, due timers,
run deadline and host claim move to AgentRunners in runner.ts. Everything
the runner needs of the device, community and storage comes through a
RunnerHost, which AgentsService implements. No behavior changes: the
existing service tests pass unchanged.

This lets a standalone host run agents with the same code as the app.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Signed-off-by: Logan Johnson <loganj@squareup.com>
The Claude runtime read the channel, conversation, names and memory
through the owner's app connection. It now asks an AgentReads port for
them, which the app fills from that same connection (ownerReads), so its
behavior is unchanged. A standalone host can give it reads made as the
agent instead.

The run part of the type (models, run and the hand-over deadline) moves
to run.ts, which imports no views, and fromOwner moves next to the
attestation check it uses, so the run code no longer imports the agents
service.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Signed-off-by: Logan Johnson <loganj@squareup.com>
The Codex runtime read the channel, conversation and memory through the
owner's app connection, like Claude did. It now uses the same AgentReads
port, filled by ownerReads in the app, so a standalone host can run it
with reads made as the agent. The port's memory read takes a signal; each
runtime keeps its own bound (Claude five seconds, Codex its turn).

The run part of the type moves to run.ts, as Claude's did.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Signed-off-by: Logan Johnson <loganj@squareup.com>
A stop while a turn read its channel deleted the agent's entry, but the
turn then created a fresh one and could reopen Codex. Recheck the
delivery's signal after the read.

The live check still passed a bare relay as its reads, so the first run
failed on the missing workflow authority. Give it the owner's reads, and
load ?raw imports and workingUntil as the app does.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Signed-off-by: Logan Johnson <loganj@squareup.com>
Nothing reads what it resolves with, so the type says so.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Signed-off-by: Logan Johnson <loganj@squareup.com>
Pins that the session list gets channel names from AgentReads, as it did
from the owner's session before.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Signed-off-by: Logan Johnson <loganj@squareup.com>
The reads carry their connection, and the shared memory reads keep one
read per connection and agent. Codex also checks the connection again
once a read settles. This test pins that last check: a read through a
connection that was replaced while it ran must not reach the turn, such
as when a different owner signs in to the same community.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Signed-off-by: Logan Johnson <loganj@squareup.com>
@loganj
loganj force-pushed the agent-host-runner branch from dd1c24f to 1b5e8d7 Compare October 14, 2026 14:00
@loganj
loganj added this pull request to stack #1080 October 14, 2026 15:00
@loganj
loganj merged commit 3c2d4cf into main Oct 14, 2026
104 of 111 checks passed
@loganj
loganj deleted the agent-host-runner branch October 14, 2026 15:14
johnmatthewtennant added a commit that referenced this pull request Oct 14, 2026
* commit '506ac5d4d':
  Fix JPEG uploads and attachment control overlap (#1068)
  feat(channels): read, write and look up channel labels (#964)
  refactor(agents): separate the agent runner and its reads from the desktop app (#961)
  fix(media): show desktop animated avatars and match dev broker media rules to native (#990)
  ci(browser): split journeys across nine shards per engine (#989)
  test(inbox): open the edit journey's conversation by name (#997)
  test(voice-notes): keep each attachment's own type in the upload fixture (#999)
  Stabilize main: consolidate WebKit flake and bottom-follow fixes (#1039)
  Push main-pane threads in and hold threads behind a loading mark (#1018)
  [buzz-app] Show full sent images and GIFs (#886)
  feat(agents): say the agents this copy runs are online (#862)
  [buzz-app] Keep the sidebar in place when switching to Personal space (#945)
  fix(builderlab): link the Buzz key before attesting remote agents (#978)
  feat(agents): show agents typing to everyone while they work on a mention (#998)

Signed-off-by: John Tennant <jtennant@block.xyz>
johnmatthewtennant pushed a commit that referenced this pull request Oct 14, 2026
* origin/main:
  fix(linux): use slight GTK hinting for clearer font spacing (#1047)
  Make Me conversations feel like project chats (#1005)
  fix(codex): deliver custom instructions without codex truncation (#1059)
  test(browser): fix the notification-reveal and nested-replies WebKit flakes (#1087)
  fix(messages): keep following after a notification reveals the latest row (#1085)
  Add native nsec backup HTTP client (#995)
  feat(buzz-mcp): add channel and member management tools (#1055)
  fix: show agent avatars in channel template editor (#1013)
  Fix JPEG uploads and attachment control overlap (#1068)
  feat(channels): read, write and look up channel labels (#964)
  refactor(agents): separate the agent runner and its reads from the desktop app (#961)
  fix(media): show desktop animated avatars and match dev broker media rules to native (#990)
  ci(browser): split journeys across nine shards per engine (#989)
  test(inbox): open the edit journey's conversation by name (#997)
  test(voice-notes): keep each attachment's own type in the upload fixture (#999)
  Stabilize main: consolidate WebKit flake and bottom-follow fixes (#1039)

Signed-off-by: Sol <49aa1f65411fd096d2e2ec144f1e7aa36fdc76d1b907cfdf7be000c66f9d3b8e@buzz.block.builderlab.xyz>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants