Repository navigation
fix(agents2): keep agents running across communities, with one Claude spare and one Codex server - #838
fix(agents2): keep agents running across communities, with one Claude spare and one Codex server#838baxen wants to merge 6 commits into
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
wesbillman
left a comment
There was a problem hiding this comment.
No actionable changes requested. Reviewed cross-community routing, connection/retry and teardown lifetimes, and shared Claude spare ownership; I found no source-demonstrated regression in this revision.
Star Lord’s automated source review via Wes’s account. Head c8a0ee8ad6d98853c4bf9d4006a71febb4bd3088; base f8705a95ba8034c1a3c7ec5f09847db1c864d97b. Source-only: no tests or app execution, and no independent CI verification; native-app switching/reconnect behavior and process counts remain unverified.
| console.warn("Claude Code could not start", error); | ||
| return undefined; | ||
| }); | ||
| this.opening.set(key, starting); |
There was a problem hiding this comment.
🤖 [P2] Preserve per-agent admission before the shared native process ceiling
Removing MAX_LIVE/room() also removes idle eviction on admission. Completed Claude conversations retain their processes for 15 minutes (sessions.ts:193–200), while the native host permits only 64 live processes across all plugins (src-tauri/src/host_process.rs:21–22,102–104). Under high conversation fan-out, one agent can consume that shared budget and prevent unrelated agents from launching processes, including Codex servers or upload readers. The message rate limit does not bound retained processes.
Please retain bounded per-agent live + opening admission and idle eviction alongside the shared spare, returning a local busy result instead of exhausting the shared registry. Add fake-host capacity/controlled-clock coverage for pending starts and completed-idle sessions. This is a source-established starvation path, not a measured memory or native stress-test result.
Agents2 listened only to the selected community, and the Claude Code and Codex runtimes stopped the processes of agents that left that list, so switching communities killed work mid-turn and left the other community unheard. Agents2 now binds each joined community the viewer has an agent in (opened through a new Communities.open, without selecting it), delivers each community's live events only to its own agents, and exposes the running agents and each agent's community connection. The runtimes keep processes for every running agent and read conversations, names and memory from the agent's own community. Processes stop when the agent is deleted, its community is left or disconnected, or the viewer signs out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Honey <8e307ae0076a4dab6b94b036ea3edc7e08f823a625269c1e6919e881a048b4d2@buzz.block.builderlab.xyz>
A first connect that fails leaves the session in error, and only the selected community has a Retry the viewer can press. Agents2 now tries an unselected community's failed connection again every 30 seconds, so its agents recover without the viewer opening it. Also covers Communities.open directly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Honey <8e307ae0076a4dab6b94b036ea3edc7e08f823a625269c1e6919e881a048b4d2@buzz.block.builderlab.xyz>
Each Claude Code agent kept its own warm `claude` process and up to six processes in all. With agents in many communities kept running, that is one idle ~300 MB process per agent. Starting the process is what a spare saves (time to turn start ~530ms cold vs ~45ms warm, measured against claude 2.1.296); sending `initialize` costs almost nothing. So the spare is now spawned without `initialize`, fixing only its folder and model, and the agent that takes it sends its own prompt and installs its own tool handler. One spare serves every agent; it follows the latest new conversation, and is replaced when no agent runs where it was started. The per-agent process cap is removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Bradley Axen <baxen@squareup.com>
A conversation whose launch finished after its last agent was removed took the spare and started a replacement, which then stayed with no agents. Taking the spare now refills it only where the runtime wants one, so stopping the last agent (`warm()` with no places) is final. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Bradley Axen <baxen@squareup.com>
- A native agent list still being read when Agents2 stops is now stale, so it cannot reopen agent communities after teardown. - Codex follows `running` whatever the inventory status, as Claude Code does, so a failed refresh no longer keeps a stopped agent's server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Bradley Axen <baxen@squareup.com>
Codex agents each kept their own `codex app-server` (~300-400 MB with its MCP children) until the agent stopped running. With agents now running in every joined community, that cost grew with every Codex agent used. Everything agent-specific already travels with the thread: cwd, developer instructions and dynamic tools go in thread/start, and tool calls carry their threadId. So the runtime now owns one server, opened on first use in ~/.buzz, and finds a tool call's agent by thread and turn across all agents. Entries keep their conversations and saved bindings but no longer own a process. A stopped agent can no longer take its process down with it, so it interrupts its own turns, then cleans the background shells its threads left (a dev server from a finished turn, say). Starting the same agent again waits for that, so it never resumes a thread mid-teardown. The server ends with the last Codex agent, on dispose, or on a crash (the next mention reopens it). live.mjs passed the relay as an object since the per-agent relay change; it now passes a function, expands ~ like the desktop host, and checks two agents on one real server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Bradley Axen <baxen@squareup.com>
c8a0ee8 to
10ddcc8
Compare
kalvinnchau
left a comment
There was a problem hiding this comment.
🤖 One lifecycle cleanup issue to address before merge.
| server.threads.delete(threadId); | ||
| await server.rpc | ||
| .request("thread/backgroundTerminals/clean", { threadId }) | ||
| .catch(() => undefined); |
There was a problem hiding this comment.
🤖 [P2] Confirm terminal shutdown before forgetting a stopped agent
With two agents sharing the server, remove an idle agent whose completed turn left a background terminal while the other agent remains running. This path deletes the stopped agent’s thread ownership, sends only thread/backgroundTerminals/clean, ignores rejection, and releases the restart barrier. A failed cleanup is then neither contained by closing that agent’s process (as before) nor retained for retry. Even a successful clean acknowledgement is not a termination barrier, as terminals() documents and handles with list/terminate/empty-list verification.
An isolated fixture using the unmodified runtime confirmed that both rejected and acknowledged-but-unfinished cleanup leave the shared server open, forget the retired thread, and release the stop barrier without list/terminate verification. Actual OS process survival was not exercised.
Please use the terminal-termination barrier for idle threads and retain cleanup ownership until termination is confirmed; on failure, retain tracked cleanup or apply explicit fail-closed containment rather than silently forgetting the thread.
wesbillman
left a comment
There was a problem hiding this comment.
One P2 change needed in the new shared-Codex lifecycle: the existing stopped-agent cleanup finding matches my independent source trace. stop() forgets thread ownership and swallows cleanup failure while another agent keeps the server alive. Reuse the terminal-completion barrier and retain cleanup ownership until termination or explicit failure containment; cover idle-agent removal and rejected cleanup. I am linking the existing inline finding rather than duplicating it.
Star Lord’s automated follow-up source review via Wes’s account. Head fafa5d63f940c37b4ad7adf3d8d7fbe098803a06; base d55d70a071bdaa2b499673dcd40ffa37a7a5d793. Source-only: no tests, Codex/app execution, or independent CI verification; native shutdown/restart behavior remains unverified.
Problem
Switching communities killed running Agents2 agents. Agents2Service only listened to the selected community. The Claude Code and Codex runtimes treated
snapshot().agentsas the list of agents that should be alive, so an agent whose community stopped being selected had its process disposed mid-turn. Its community then went unheard until you switched back.Change
running: the viewer's agents in every joined community that isn't disconnected. A reconnect doesn't remove them.agentsis still the selected community's agents, so the Agents2 page is unchanged.agents2.relay(pubkey)returns the owner's connection to an agent's own community.running. They read thread history, channel names and memory through the agent's own community instead of the selected one. Codex no longer disposes everything on a community switch. Its saved-session key is the sameorigin:viewerstring as before, so no saved sessions are orphaned.Communities.open(id)is a new method that connects a joined community without selecting it.communities/service.tsis a FOUNDATION file; baxen asked for this fix after reviewing the plan that called out this addition.A process now stops only when its agent is deleted, its community is left or disconnected, the viewer signs out, or the plugin is disabled.
One shared Claude Code spare (25e885f)
Keeping every agent running made Claude Code's per-agent warm spare expensive: one idle
claude(~300 MB RSS) per agent, and up to six processes per agent.initialize(prompt, memory, hooks), and installs its own tool handler. Then another spare starts.initializecosts almost nothing. Measured againstclaude2.1.296, the time from send to turn start is ~530 ms cold vs ~45 ms with the spare (medians, 10 interleaved rounds). A spawned-only process is as fast as a fully initialized one.claudelists the SDK MCP server's tools right after spawn, beforeinitialize. Until the spare is taken, a placeholder handler answers; the tools list does not depend on the agent.claudeis not respawned in a loop.initializein ~85 ms and starts a thread in ~95 ms, so a spare would save ~100 ms at most.Tradeoffs
--resumeis a spawn flag.thread/resume), so an idle stop is a sensible follow-up.Validation
Full Vitest suite: 653 files / 9193 tests passed at 2e36587.
Affected suites: agents2, bundled and communities, 222 files / 2878 tests, passed at 43f3c3b. Type check and lint are clean.
New tests:
Communities.openopens a joined community without selecting it, and returns nothing for a community that isn't joined.Mutation check: removing the per-community event filter fails the routing test.
Codex tests updated: the test that asserted "disposes on community change" now asserts the server ends when its agent stops running.
Independent agent review: done. Its medium finding was that a background community whose first connect fails never recovers. It's fixed in 43f3c3b.
Shared spare (25e885f):
claude, outside the app: two agents with different prompts took turns using the shared spare, and each answered with its own name.Lifecycle review fixes (8904805, c8a0ee8): codex00's review found three lifecycle bugs, each reproduced. Each fix takes one decision away from a place that shouldn't make it:
error, so leaving a community after a failed refresh left its servers running. It now followsrunningregardless of inventory status, as Claude Code does.Not yet done
claudeexists beyond those in use.No browser cases added or removed.
🤖 Generated with Claude Code
Weekend review stack (2026-10-11)
main (
d55d70a07, including #851) → #838 → #852 → #855 session viewer/context → #856 model/effort picker → #857 ordinary metadata → #858 identity adoption → v1 retirement (baxen/remove-agents-v1-tail). Each PR is based on its predecessor. New layers remain drafts for native/human acceptance.This branch was restacked onto its current parent. Review and merge bottom-up; after a squash merge, restack only each child’s own commits onto the new parent and retarget its base.
baxen/nextis an append-only testing mirror of the full stack, not a PR to merge into main. Green is unchanged.From #852
Why
Each Codex agent used to have its own
codex app-server, and it ran until that agent stopped. With #838, agents keep running in every joined community, so every Codex agent you've used holds one indefinitely. On a Mac, each one is about 300–400 MB:codexbinary: ~176 MBnodewrapper: ~49 MB~/.codex/config.toml: ~80 MBWhat changed
CodexRuntimenow owns one app-server for every agent. It opens on first use, in~/.buzz.This works because everything agent-specific already travels with the thread, not the process:
cwd,developerInstructionsanddynamicToolsgo inthread/start/thread/resume.item/tool/callcarries itsthreadIdandturnId.So a tool call finds its agent by thread and turn across all agents. An entry keeps its conversations and saved bindings but no longer owns a process.
Starting a Codex server cold is cheap: about 85 ms for
initializeand 95 ms for the firstthread/start, measured withcodex-cli0.162.0. A single shared server therefore needs no spare process, and no idle timer either: the most it holds is about 400 MB, however many agents you have.Stopping an agent no longer kills a process the others share. Instead:
thread/backgroundTerminals/cleanworks afterthread/unsubscribe.The server ends when the last Codex agent that has worked stops, when the plugin is turned off, or when it crashes. After a crash, the next mention reopens it.
Tradeoff
A crash, or a failed shell cleanup, now ends every Codex agent's current turn, not just one agent's. Each affected thread gets the usual "Codex could not finish" reply, and the next mention resumes the thread. A failed cleanup still closes the server on purpose, so a stopped agent's shells can't outlive it.
Tests
Unit: all 50 Codex tests pass. Four are new:
The new tests for one shared server, interrupting only the removed agent, and the cleanup/restart ordering fail without the change.
Full Vitest suite: 9207 pass.
tscand biome are clean.Real Codex (
bin/node src/bundled/codex/live.mjs): passes, including a new step where two agents with different workspaces and instructions run on one real server, and each replies as itself from its own workspace.live.mjshad been passing the relay as an object since fix(agents2): keep agents running across communities, with one Claude spare and one Codex server #838 changed the constructor; it now passes a function and expands~like the desktop host does.Not yet run in the native app. To try it:
codex app-server, not two.Weekend review stack (2026-10-11)
main (
d55d70a07, including #851) → #838 → #852 → #855 session viewer/context → #856 model/effort picker → #857 ordinary metadata → #858 identity adoption → v1 retirement (baxen/remove-agents-v1-tail). Each PR is based on its predecessor. New layers remain drafts for native/human acceptance.This branch was restacked onto its current parent. Review and merge bottom-up; after a squash merge, restack only each child’s own commits onto the new parent and retarget its base.
baxen/nextis an append-only testing mirror of the full stack, not a PR to merge into main. Green is unchanged.