Repository navigation
Conversation
f0e5288 to
63721af
Compare
63721af to
827cf93
Compare
827cf93 to
a848c82
Compare
2469ead to
ad53f6f
Compare
ad53f6f to
fde4b45
Compare
fde4b45 to
d03ce7f
Compare
d03ce7f to
a9a56dd
Compare
a9a56dd to
a474233
Compare
a474233 to
eab5259
Compare
176fb8d to
d730f9c
Compare
d09a5c6 to
661ac1f
Compare
3880d2c to
c726d6d
Compare
This comment has been minimized.
This comment has been minimized.
wesbillman
left a comment
There was a problem hiding this comment.
🤖 Star Lord’s automated source review via Wes’s account.
No new actionable findings in this follow-up; assessment 9/10 for the signing fix and rebase integration.
The standalone host now accepts shared-note kind 30023, matching the app custody allowlist. Its existing parity test still compares all 15 kinds, and kind 1 remains a valid rejected example. The shared note save/readback/conflict and delete paths retain their existing behavior. Earlier membership-recovery and Solo Canvas fixes are preserved: range-diff shows ten unchanged patches, one adapted only to Codex tab context, and the new signing patch.
Head cfdb393cdfad16078482a37cf55089277f2db00c; base 24bc02a81cc54c9a40d0bab6be8013235c8ddf3b. Follow-up to reviewed head da2c10ddacdd3740bcca1ccca2a90e37430fac5c, not a fresh exhaustive audit of unchanged code. Rocket independently checked systems/lifecycle, Mantis correctness/security/signing parity, and Groot product/error-retry flows; Star Lord integrated the evidence. The PR’s documented takeover and persistence limitations remain unchanged.
Public description (no image attachments) and new commit material inspected; no new disclosure finding. Source-only: no PR code, tests, builds or apps executed. One CI snapshot showed successful JavaScript/Rust checks and some browser jobs still running; Windows was skipped. Native handover, deployed relay acceptance/limits, performance, rendered focus/accessibility and human acceptance remain unverified. COMMENT only; no approval or merge authorization.
Add buzz-agent-host, a Node script that runs one agent this machine's app saved, with the same AgentRunners and Claude/Codex run code the app uses. It reads the agent's identity from identities.json and its key from BUZZ_AGENT_KEY or the app's Keychain bundle, listens on the agent's own relay connection, reads the community as the agent, and follows the app's hosts/ flock protocol so only one copy runs the agent. It uses the app's agent storage (agents/state/): the agent runs with the settings, team prompts and Claude or Codex sessions the app saved, reads its record again every tick so edits made in the app apply, and writes only its own agent's record. Build with `pnpm agent-host`; run with `node dist-agent-host/buzz-agent-host.mjs run <pubkey>`. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Signed-off-by: Logan Johnson <loganj@squareup.com>
A DM's first messages wait for its metadata, so an untagged message from the owner wakes the agent as in the app. Subscriptions the relay closes after the login are asked for again, with a growing wait, unless it refused them. Channels are heard in batches of 128, the most the relay accepts in one request. When a process exits, stopped or not, its process group is killed too, as host_process.rs does, so a tool it left running cannot outlive the agent's claim. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Signed-off-by: Logan Johnson <loganj@squareup.com>
- While one subscription is asked for again, the open ones still hear messages. The feed now holds them until it is listening again, and then gives them to the host. Before, the host dropped them. - Each subscription that closes keeps where it stopped hearing. Its new request asks from there, until the relay has sent what it missed. Before, traffic on another subscription moved that point on, so a long outage skipped messages. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Signed-off-by: Logan Johnson <loganj@squareup.com>
The feed gave what it heard while not listening as live events. The runner marks a live event seen before it queues it, so a burst past the agent's queue (32) or rate budget was dropped for good. Those events, and what a re-requested subscription sends before its EOSE, now come as a replay. The runner keeps a replay in its backlog and admits it as the agent has room. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Signed-off-by: Logan Johnson <loganj@squareup.com>
Catch-up progress now has one key per agent, so the old reason, a shared value the app rewrote whole, no longer applies. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Signed-off-by: Logan Johnson <loganj@squareup.com>
The buzz.solo label on channel metadata is the one Solo fact, which every member and agent reads. The host now reads it fresh for each turn, so an agent taken over from the desktop answers an existing Solo chat as one top-level conversation, and threads again once the chat becomes a channel. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Signed-off-by: Logan Johnson <loganj@squareup.com>
…batch The relay refuses a channel batch only once the agent can read none of its channels. The feed stopped being live until the next connection, so mentions and timers in every other channel stopped. A refused batch no longer counts toward being live, and is asked for again when a roster shows the agent back in one of its channels. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Signed-off-by: Logan Johnson <loganj@squareup.com>
After stopAll kills a process group, an orphan that has exited can stay a zombie until its new parent reaps it, and signal 0 still finds it. The test now reads the process state, so it no longer fails on CI runners whose reaper is slow. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Signed-off-by: Logan Johnson <loganj@squareup.com>
The app now hands each Solo chat turn the chat's Canvas: its standing instructions and the folders the agent works in. The standalone host gave its runtimes no Canvas reader, so after the desktop app closed, a Solo chat lost those instructions, Claude started without the chat's folders, and an edited or cleared Canvas never replaced the one a resumed conversation knew. The host now reads the chat's newest Canvas as the agent, for each turn, and hands it over only when the owner or an agent they attest wrote it, with the same check the app uses. No Canvas tells the agent that there is none; a failed read keeps what the turn knew. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Signed-off-by: Logan Johnson <loganj@squareup.com>
A turn read its chat's Canvas with an ordinary query, which a relay with replica reads may answer from a replica that lacks the owner's newest edit or clear. Ask for it with consistency: strong, as the app's Canvas reads do; the relay's HTTP bridge then answers from its writer. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Signed-off-by: Logan Johnson <loganj@squareup.com>
Two membership changes left a channel unheard. A new channel added to a batch the relay had refused was never asked for. And a rejoined channel whose batch was still open for its other channels was not asked for again: the relay drops a channel the agent leaves from the subscription without closing it, and the feed hears no roster that leaves the agent out, so it could not tell. Now a new channel, or a new roster of a channel the agent is in, asks for its batch again as it is now, even if the relay refused it. A roster heard again, as after a reconnect, asks for nothing. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Signed-off-by: Logan Johnson <loganj@squareup.com>
Main's #1285 lets the app's agent custody sign kind 30023 so agents can publish shared notes. The host's list must stay equal to it, and the test that pins the two lists caught the difference after the rebase. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Signed-off-by: Logan Johnson <loganj@squareup.com>
Main replaced the runner's Jev hooks with decisions and keyStatus, added interrupted turns and gitAuth to the runner host, and lets custody sign repository announcements (30617). The host keeps interrupted turns in memory, as it keeps catch-up progress, and reports that Git access needs the desktop app. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Signed-off-by: Logan Johnson <loganj@squareup.com>
The host had its own relay subscription client (feed.ts). The app already has one: subscribeRelayTraffic in features/relay/live.ts, which the desktop's agents hear through. The host now runs that client, logged in as the agent with its owner's attestation, and the app's recovery owner retries what fails. What the host adds is small (listen.ts): it reads the agent's channels from its rosters, with their types for DMs, and gives them to the client's update(). It reads them again when the relay says the agent's membership changed, when a channel is denied, and at the client's periodic repair. Until the first read, membership is unknown, so the runner holds watched events, and releases them once the read is done. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Signed-off-by: Logan Johnson <loganj@squareup.com>
The host no longer has its own live client and reads. It signs in to the app's RelaySession as the agent (session.ts), with no writer and no publishing on its socket. The agent's reads are the app's ownerReads on that session, and its roster checks are the service's, moved to roster.ts so both use one copy. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Logan Johnson <loganj@squareup.com>
The host had its own copy of the agents service's ordered change queue. Both now use RecordChanges: one agent's changes run in order, timer progress is queued behind them, and timers see progress not saved yet. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Logan Johnson <loganj@squareup.com>
buzz-agent-host now starts `<app> agent-host <pubkey>`, a headless mode of the app's own executable that answers the app's agent commands as JSON lines over stdin and stdout, with the same Rust code the window uses. The agent's key no longer enters Node, and the Node copies of the claim, storage, identity and kind checks are gone. The helper adds login, read auth and memory reads as the agent, and Git access works. Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Logan Johnson <loganj@squareup.com>
Summary
buzz-agent-host, a Node program.buzz-agent-host run <pubkey>runs one Claude or Codex agent that the app saved on this computer. It uses the same run code as the app (refactor(agents): separate the agent runner and its reads from the desktop app #961) and the same saved state (feat(agents): keep agent state in files that a program outside the app can read #974): the agent's settings, watches, timers, team instructions and Claude or Codex sessions. So the agent answers mentions, follows its watches, runs its timers and continues its conversations the same way in both places.<app> agent-host <pubkey>, and asks it to sign each request. That mode runs the same Rust code the app's window uses for its agents: identities, Keychain, hosting locks, storage and the allowed event kinds.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 and the same saved state as the app, so the agent behaves the same wherever it runs. The app keeps working exactly as it does today.
Stack
#961 and #974 are merged. Each PR keeps the app working.
buzz-agent-host, the program that runs an agent with the app closed.How to try it
BUZZ_APPnames the app to use: aBuzz.appbundle, or an executable such as a dev build'ssrc-tauri/target/debug/buzz-foundation. Without it, the program uses/Applications/Buzz.app. A bundle must have the identifierdev.local.buzz.foundation, so another app named Buzz is refused.Details
src-tauri/src/agent_host.rs,src/agent-host/custody.ts):main.rschecks foragent-hostbefore Tauri starts. That mode reads one JSON request per line on stdin and writes one answer per line on stdout. It answers the same commands the app's window uses (app_agent_claim,_publish,_query,_upload,_media,_git_auth,_remember,_state_*and the others), through the same functions: each Tauri command is now a thin wrapper around a function that takes&AppAgentHost. It serves only its own agent, except for the agent list (for team names) and the shared state files. Other commands, such as create or delete, are refused. On the Node side,agentsNativeand the state storage now take aninvoke, so the program uses the app's own TypeScript wrappers with the helper'scall.app_agent_loginsigns the relay login (kind 22242) for the agent's own community only.app_agent_read_authgives the NIP-98 header for the agent's/queryand/profiles/searchreads only.app_agent_memoriesreads the agent's memory with its own key. The NIP-44 key between the agent and the owner is the same from both sides, so the decoder now picks the other side from the key it is given.Hosting::claim), as a dev build does, so it steps aside for the installed app. It exits when the program closes its stdin, and the OS then releases its locks, also after a crash. If the helper stops on its own, the program stops, because the agent is no longer held.createRelaySession, the one the desktop's owner session uses), signed in as the agent with the owner's attestation (src/agent-host/session.ts). That session keeps the agent's channels, rosters and Solo labels, hears them live and recovers as the app does. The agent's reads are the app'sownerReadson that session. The session cannot publish: it gets no writer, and its socket sends only the login and subscriptions (a test checks this).app_agent.rs).app_agent_git_auth.features/agents/roster.ts) and the ordered queue of record changes and timer progress (features/agents/record-changes.ts) moved out of the agents service, so the app and the program use one copy.claude, orcodex app-server), with the samePATHthe app uses.agents/state/files (feat(agents): keep agent state in files that a program outside the app can read #974) through the helper. It keeps a copy and reads the files again at each 5-second check, unless one of its own writes is still pending. So a change the owner saves in the app takes effect without a restart. A session that one side starts, the other continues.buzz-agent-host, because the app already ships a different program calledbuzz-agent.Same as the app
self), as the app does, and checks the workflow creator's signed profile.classifier()is "unavailable" in both), so classified watches behave the same.Security
<app> agent-host <pubkey>and have it sign as that agent, but only the event kinds the app allows for agents. It cannot get the key. Before this change, the program read the raw key from the Keychain with thesecuritycommand, which is worse.Not in this program
Known limits (from adversarial review; not fixed here)
.packagedshared for a moment. If the installed app tries its lock at that moment, it still runs the agent and tries again later, so both can answer one event. A dev build of the app already checks the same way, so this PR keeps that behavior./Applications/Buzz.appmust be a build that includes this PR.🤖 Generated with Claude Code