Repository navigation
Providers: a switch to turn native subagents off - #1046
Conversation
|
CI on bad52c4: every job that runs on this PR has passed on this commit at least once, over four runs. The latest run's only failure is The other failures along the way:
A re-run of the failed job would settle it without running the rest again. |
7363cce to
684cabe
Compare
Settings → Providers gets a "Native subagents" switch for Claude Code, per device (zeronsh#1003). Off, a top-level chat launches Claude Code with `--disallowedTools=Agent,Task,Workflow` and delegates through Zeron side chats instead. A bare tool name "removes the matching tools from Claude's context" (https://code.claude.com/docs/en/cli-reference), so no refused call is left running (zeronsh#893). `Workflow` runs agents of its own (zeronsh#804). Zeron's MCP server tells the agent to delegate with `create_chats`. A chat that has a parent keeps its provider's own subagents: delegation is one level deep, so a side chat can't create chats. So does a chat whose run reached its host before its row did, until the row lands. The switch rides the run's Zeron MCP server env (`ZERON_SIDE_CHATS_ONLY`), so the drivers read it from the request. A session parked between turns that started the other way is replaced before its next turn once it holds no background work, like one whose CLI was updated in place. A device that can't report the switch (an engine predating it) shows the row disabled, with the reason. Codex isn't offered yet. Its driver starts a side-chats-only thread without agent tools, but a Codex thread keeps the multi-agent mode it ran with, so turning the switch back on wouldn't give a chat that ran while it was off its agent tools back. CI now runs the engine's `side_chats` tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The device_routing test and the mcp_standalone_smoke example build a Zeron MCP Origin by hand, and neither got the side_chats_only field this branch adds, so CI's voice job stopped on E0063 compiling device_routing. Both take the default now, as the literals in crates/mcp/src/tools.rs do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: zeronsh[bot] <346184242+zeronsh[bot]@users.noreply.github.com>
… createChat lands A run that outruns its chat's row keeps its subagents while the claimed row can't say whether the chat has a parent. The switch is meant to apply again from the next turn once the row's createChat lands; this pins that parent_unknown flips back when the full row, with a null parentChatId, arrives. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: zeronsh[bot] <346184242+zeronsh[bot]@users.noreply.github.com>
684cabe to
f65c21d
Compare
There was a problem hiding this comment.
Security audit: clean. No new dependencies, build scripts, workflow changes or network hosts. The feature only removes agent tools (--disallowedTools=Agent,Task,Workflow), and the two new RPCs use the existing forwardable provider-settings path. The demo PNGs have no trailing data.
Review: the setting persists in harness-prefs.json and defaults to on. It reaches Claude Code through the run's Zeron MCP server env and the CLI flag. It applies only to top-level chats, including once a claimed row's createChat lands, and a parked session is replaced at its next idle turn. The Delegation row reuses the Completion section's row, switch and theme tokens.
Maintainer changes: rebased onto current main. The only conflict was scripts/ci/test-core.sh, now the union with #1043/#1045. I also added a test, a_claimed_chat_knows_its_parent_once_its_create_lands, for the claimed-row transition.
Local (Linux, rebased head): engine side_chats/restart_resume/claude_subagents/device_routing/e2e, harness lib/claude/codex, mcp, doc, rpc and UI settings:: all pass, and cargo check --all-targets is clean. Clippy has no findings on the PR's lines. 4 engine lib tests fail identically on main because the Cloud machine's env leaks into them.
Providers gained Native subagents (zeronsh#1046), and its per-provider Completion and Updates rows were never indexed. Index all four; a hit lands on the row when its provider is expanded, else on the provider list to pick one. The agreement test now also draws an expanded provider with every section loaded, so a new per-provider row fails it like any other page's. The clear button gets a tooltip and focus ring like the other icon-only buttons, the empty state is the palette's, and the magnifier takes a nav glyph's slot so the query lines up with the section names. ⌘F now only waits for an account menu that is open, not one playing its exit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: zeronsh[bot] <346184242+zeronsh[bot]@users.noreply.github.com>
Providers gained Native subagents (zeronsh#1046), and its per-provider Completion and Updates rows were never indexed. Index all four; a hit lands on the row when its provider is expanded, else on the provider list to pick one. The agreement test now also draws an expanded provider with every section loaded, so a new per-provider row fails it like any other page's. The clear button gets a tooltip and focus ring like the other icon-only buttons, the empty state is the palette's, and the magnifier takes a nav glyph's slot so the query lines up with the section names. ⌘F now only waits for an account menu that is open, not one playing its exit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: zeronsh[bot] <346184242+zeronsh[bot]@users.noreply.github.com>
* Settings search: find any setting from the sidebar, with ⌘F A field under Back searches every Settings page: its 53 settings rows, each shortcut where it is recorded, and the pages. Hits replace the section list; a pick opens the page, glides the row into view and washes it with the accent. - settings/search.rs: the index kept beside the pages, ranked after the palette's thread search, with typos below every real match. - settings/search/reveal.rs: rows carry a SearchAnchor and each page a page_reveal, so landing keeps no state on the pages. - shell/settings_search.rs: the field, its hits, ⌘F, keys and focus. - the_index_and_the_pages_agree holds the index and the pages in step. - docs/screenshots/settings-search: frames from the new fixture example. * Settings search: ⌘F leaves an open dialog the keyboard ⌘F moved focus to the search field even while a dialog was open over the page (theme import, device rename, Delete all, a provider login, the sync overlay or the account menu), leaving the dialog without the keyboard. Each page now says whether it has a surface Escape would close, read-only beside dismiss_on_escape, and ⌘F waits until it closes. Also keep the field's magnifier from shrinking, as the Files search does, and write the typo matcher's sentinel as a byte string for clippy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: zeronsh[bot] <346184242+zeronsh[bot]@users.noreply.github.com> * Settings search: find each provider's own rows, and match the palette Providers gained Native subagents (#1046), and its per-provider Completion and Updates rows were never indexed. Index all four; a hit lands on the row when its provider is expanded, else on the provider list to pick one. The agreement test now also draws an expanded provider with every section loaded, so a new per-provider row fails it like any other page's. The clear button gets a tooltip and focus ring like the other icon-only buttons, the empty state is the palette's, and the magnifier takes a nav glyph's slot so the query lines up with the section names. ⌘F now only waits for an account menu that is open, not one playing its exit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: zeronsh[bot] <346184242+zeronsh[bot]@users.noreply.github.com> --------- Co-authored-by: Wing <44917547+wingleeio@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: zeronsh[bot] <346184242+zeronsh[bot]@users.noreply.github.com>
Part of #1003 (Claude Code only).
Settings → Providers gets a Native subagents switch for Claude Code, per device. Off, a top-level chat starts Claude Code without its own subagent tools, and Zeron's MCP server tells the agent to delegate through side chats instead. A side chat keeps its subagents: delegation is one level deep, so it can't create chats of its own. On is the default and nothing changes for it. Codex isn't offered yet (see Known limits).
What changes
harness-prefs.json(nativeSubagentsOff,crates/engine/src/registry.rs:605,:624) and serves them over two new RPCs,GetNativeSubagentsandSetNativeSubagents(crates/engine/src/rpc.rs:2553). Both forward to a remote device like the other provider settings (forwardable,rpc.rs:2037). Only Claude Code is switchable (crates/harness/src/lib.rs:719).crates/engine/src/sessions.rs:2151). A side chat can't create chats (crates/mcp/src/tools.rs:939-948, and its instructions say so,crates/mcp/src/jsonrpc.rs:52), so it launches with its subagent tools, and its Zeron server adds no delegation rule.crates/engine/src/doc_host.rs:5580-5583), and a side chat created on another device would run its first turn side-chats-only. Until the chat'screateChatlands, which recordsparentChatIdeither way (a null for none), such a chat keeps its subagents (crates/engine/src/workspace_host.rs:725,crates/doc/src/registry.rs:1335). Once it lands, the switch applies from the next turn.--disallowedTools=Agent,Task,Workflow(crates/harness/src/claude/mod.rs:374). A bare tool name "removes the matching tools from Claude's context" (CLI reference), so the model never calls them. Denying calls at run time instead would leave each refused call running forever (claude: rejected Agent call shows as a running subagent forever #893). The model calls the spawn toolAgent, while 2.1.310's init frame still lists it under its former name,Task; either name alone removes it.Workflowtool runs agents of its own (Feature request: show Claude Code Workflow runs and their agents live #804), and [Feature]: Provider setting to turn off native subagents (Claude Code, Codex) and delegate through side chats #1003'sAgent,Taskleaves it in place. [Feature]: Provider setting to turn off native subagents (Claude Code, Codex) and delegate through side chats #1003 suggested a second switch for it. Its agents are native subagents too, the ones Off replaces with side chats, so the same switch removes it.ZERON_SIDE_CHATS_ONLY=1(docs/mcp.md:25-30). The drivers read it from the request. The server reads it once at start, with the chat and device ids (crates/mcp/src/zeron.rs:53), and adds one sentence to itsinitializeinstructions: delegate withcreate_chatswhere you would start a subagent (crates/mcp/src/jsonrpc.rs:65-71,:190). An engine that serves no IPC port stamps no Zeron server, so its runs keep their subagents (crates/engine/src/sessions.rs:2177, noted at the run's flag,:199-203).crates/engine/src/sessions.rs:1200). So each chat picks the change up at its next turn once its background work is done, as the row now says (crates/ui/src/settings/delegation.rs:13). The check asks dispatch's question, so a parked side chat stays warm. The startup-crash retry now takes the request before the MCP stamp, so its own dispatch stamps it with the setting of its time (sessions.rs:3436).delegation.rs:98,crates/ui/src/settings/worktrees.rs:199): the server runs each request in its own task (crates/rpc/src/server.rs:49), so a second one could land first. A refused change shows the device's error and reloads. A device that can't answer keeps the section, its switch inert at the default, with the reason as its line. An engine that predates the RPCs gets "Update Zeron on the selected device to change this.", commit credit's wording (worktrees.rs:173-174), and any other error shows as is (delegation.rs:18,:120). A Cloud target hides the section: its machines use the defaults (delegation.rs:35).scripts/ci/test-core.sh:27now runs the engine'sside_chatstests, right afterrestart_resume; no CI job ran them. Fork: settle the running subagent chips a fork copies #1045 adds the same change, so the two merge in either order.Testing
cargo test --locked:-p zeron-engine --lib --test side_chats --test restart_resume --test e2egives 631 passed (5 ignored), 7, 13 (1 ignored) and 29 (2 ignored).-p zeron-harness --lib --test claude --test codex: 445, 35 (3 ignored) and 46 (4 ignored).-p zeron-mcp --lib: 36.-p zeron-ui --lib -- --test-threads=1 settings::: 171.cargo check --locked --workspace --all-targetsis clean, examples and every test target included. Of the targets the suites above skip, CI's voice job runs two:-p zeron-engine --test device_routinggives 11 passed,-p zeron-rpc --test device_room13 (1 ignored).a_side_chat_keeps_its_subagent_tools_when_native_subagents_are_off(crates/engine/tests/side_chats.rs:892): the real Claude driver on the fake CLI, switch off. A top-level chat launches withBashonly and a side-chats-only server; a side chat withBash,TaskandWorkflow, and a server without the rule; so does a chat whose row its run claimed. It fails atside_chats.rs:992without the parent check (left: [(["Bash"], "side chats only"), (["Bash"], "side chats only"), (["Bash"], "side chats only")]), without the unknown-parent check (left: [(["Bash"], "side chats only"), (["Bash", "Task", "Workflow"], "native subagents"), (["Bash"], "side chats only")]), and with [Feature]: Provider setting to turn off native subagents (Claude Code, Codex) and delegate through side chats #1003'sAgent,Task(left: [(["Bash", "Task", "Workflow"], "side chats only"), …]).a_parked_side_chat_stays_warm_when_native_subagents_switch(crates/engine/tests/restart_resume.rs:703): with a replacement check that ignores the parent, the side chat's warm child is replaced (restart_resume.rs:754,left: 2,right: 1).switched_native_subagents_replace_the_parked_session(restart_resume.rs:649): the same setting keeps the warm child; a switched one starts a new child whose Zeron server is side-chats-only.a_side_chats_only_run_launches_without_subagent_tools(crates/harness/tests/claude.rs:228): the fake CLI listsBash,TaskandWorkflow, then onlyBashunder the flag. With [Feature]: Provider setting to turn off native subagents (Claude Code, Codex) and delegate through side chats #1003'sAgent,Taskit fails atclaude.rs:257(left: [["Bash", "Task", "Workflow"], ["Bash", "Task", "Workflow"]]).a_side_chats_only_thread_starts_without_agent_tools(crates/harness/tests/codex.rs:2142): the Codex driver'sthread/startcarries the three keys only for a side-chats-only server.side_chats_only_instructions_add_the_delegation_rule(crates/mcp/src/jsonrpc.rs:266).native_subagents_switch_off_per_harness_and_persist(crates/engine/src/registry.rs:1460): only Claude Code is switchable, Codex is refused, and the choice persists. With Codex back in the gate it fails atregistry.rs:1468(left: [ClaudeCode, Codex]).native_subagents_show_for_switchable_agents_and_switch_at_once(crates/ui/src/settings/delegation.rs:226): without the in-flight guard, a flip during a switch goes through (delegation.rs:255,assertion failed: page.native_subagents.ready().unwrap().off.is_empty()).native_subagents_say_why_a_device_cannot_switch(delegation.rs:264): it fails at:266without theUnknownMethodwording (left: "unknown method: GetNativeSubagents"), at:281when an error hides the row (left: None), and at:285with Codex back in the gate (left: Some((true, false, "Update Zeron on the selected device to change this."))).title_run_preserves_read_only_and_replaces_coding_instructions(crates/harness/tests/codex.rs:1621): the fake Codex now checks the three agent keys in the title run'sconfig, which now comes from the sharedAGENT_TOOLSlist (crates/harness/src/codex/mod.rs:128,:1325). Without them it fails atcodex.rs:1643(error: Some("title restriction missing")).old_harness_preferences_default_to_automatic_titles(crates/engine/src/registry.rs:1555): one assert added, an olderharness-prefs.jsonturns nothing off.initialize_and_list_over_jsonrpc(crates/mcp/src/tools.rs:2939):initializecarries the delegation rule exactly when the server's origin is side-chats-only. Withinitializeignoring the origin it fails attools.rs:2957(left: false,right: true).--disallowedToolsvalue. None:Task,ListAgents,SendMessage,TaskStop,Workflow.Agent,Task,Workflow:ListAgents,SendMessage,TaskStop.Agent,Task,AgentorTaskalone: those three andWorkflow.SetNativeSubagents). Those fixes don't change these steps: the Claude Code flag and env are the same, and each chat's row came before its run.ls. Perps, its Claude process ran with--disallowedTools=Agent,Task,Workflow, and its Zeron server's env heldZERON_SIDE_CHATS_ONLY=1. ItssessionStarted(journal seq 1) listed none ofAgent,TaskandWorkflow. The agent said its subagent tools were off, calledcreate_chat, and the side chat ranls.sessionStartedlistedTaskandWorkflow, and its server'sinitializekept the one-level rule without the delegation rule (the parent's has both).native subagents switched; replacing the parked session, the new process resumed the session without the flag, itssessionStarted(seq 623) listedTaskandWorkflow, and the agent'sAgentcall returnedpong.Agent,Taskand without the parent check, same scenario: switched off,sessionStartedseq 1 lackedAgentandTaskbut listedWorkflow, and its side chat's seq 1 lackedTask.Shell, then the first and third steps.GetNativeSubagentsanswered{"off": ["claude-code"], "switchable": ["claude-code"]}. The first turn's Claude process had--disallowedTools=Agent,Task,Workflowin all 74pssamples, and the agent delegated throughcreate_chat. Switched back on over IPC, the engine logged the replacement, the second turn's new process ran without the flag (33 samples), and itsAgentcall returnedpong.c04e3749) after these runs, with no conflict. Merged there with the other five PRs of this series (Claude subagents: count and open only this chat's spawns #1041, Claude subagents: forward a foreground subagent's text and thinking #1042, Claude subagents: give a typed /skill's fork a chip and a tab #1043, Claude subagents: fill a typed fork's tab while the CLI holds it #1044, Fork: settle the running subagent chips a fork copies #1045), it passes every suite above again, plus the rest of CI's selection, sync'smock-servertests,zeron-mcpand the UI'ssettings::tests, each executable checked to be built from that merge. The one failure, preview'sbackgrounded_scrubbed_terminal_and_out_of_tree_servers_stay_attributed, fails the same way on v0.2.124 alone, on this Mac.pssamples and the agent delegated through a side chat; switched back on, the parked session was replaced, and the next turn'sAgentcall returnedpong.Shell).Known limits
disabledin its session meta (session/session.rs:866), each turn records its mode (session/turn_context.rs:600), and a resume takes the recorded mode unless the config forces one (session/mod.rs:463-479,:692-694,history/src/lib.rs:418-446). The config can only force V2 or disabled (config/mod.rs:1533-1541), and a disabled mode leaves the agent tools out (tools/spec_plan.rs:647-649). The follow-up: when the switch goes back on, start such a chat on a fresh Codex thread with its history replayed, the way a provider switch does, behind a driver capability. The engine would have to record which Codex thread ran with the switch off, since an idle reap or a restart drops the parked session that knows today. The Codex driver already starts a side-chats-only thread without agent tools (crates/harness/src/codex/mod.rs:1342, tested atcrates/harness/tests/codex.rs:2142);docs/research/codex-subagents.md:53-72has the details.crates/mcp/src/tools.rs:939-948), where native subagents nest, and a side chat keeps its provider's own subagents. Each delegation starts a full provider process with a fresh context.context: forkskill still ran as Claude's own forked subagent, its traffic tagged with itsSkillcall's id, while the two agents it starts became side chats.Agentrun without their workers while it's off:/batch, Explore, Plan. I haven't checked how each of them degrades.ListAgents,SendMessageandTaskStopstay listed. None of them starts an agent: the first two also reach your other Claude Code sessions, andTaskStopstops background tasks (tools reference).SendMessagecan also resume a subagent by its id, so a session switched off after it ran subagents might still resume one of those; not tried.crates/engine/src/registry.rs,rpc.rs, and incrates/rpc/src/lib.rsinside the conflict Settings: choose when idle sessions close #960 already has there), Experimental: edit what the agent remembers, stop one command or subagent, and a system prompt #989 (registry.rs), Side chats: move one into the sidebar as its own session #987 and Session notes: a synced Markdown scratchpad per main chat #965 (crates/engine/tests/side_chats.rs), chat3: transcript plane behind a labs flag (8% of chat2's billed writes, paged history, stored tool outputs) #979 (crates/engine/src/workspace_host.rs: its labs-flag methods go right afterWorkspaceHost::chat, where this PR addsparent_unknown,:725). Each is an addition at the same spot as one of this PR's: keep both.scripts/ci/test-core.sh:27). Claude subagents: give a typed /skill's fork a chip and a tab #1043 addsclaude_subagentsto it, and Claude: show Workflow runs as subagents with live progress #784 (with Claude: open each Workflow agent's live transcript #785 on top)workflow_progress, so each conflicts with this PR there. Whichever lands second keeps every name.SettingsSection::Harnessesarm ofpage_terms.Screenshots
Offscreen renders of the production
Shell(Window::render_to_image), attached to a headless engine, both built from this branch;docs/media/native-subagents-setting/README.mdsays how they were made. The crops stop above the provider's Accounts section, and the session card's device name is blurred. The media are in their own commit,63bd38d3, if you'd rather drop them before merging.The live chat's session card: the side chat its first turn delegated to while the switch was off, and the
Agentrun of its second turn once it was back on.Need help on this PR? Tag
@codesmith-botwith what you need.