Skip to content

Providers: a switch to turn native subagents off - #1046

Merged
zeronsh[bot] merged 5 commits into
zeronsh:mainfrom
alexandre-vl:subagents/native-subagents-setting
Oct 26, 2026
Merged

zeronsh[bot] merged 5 commits into
zeronsh:mainfrom
alexandre-vl:subagents/native-subagents-setting

Conversation

@alexandre-vl

@alexandre-vl alexandre-vl commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

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).

Settings, Providers, Claude Code: a Delegation section with a Native subagents switch, on by default, then switched off. Its line reads: Off: delegate through side chats instead. Applies to each chat's next turn once its background work is done.

What changes

  • Setting. The device's engine keeps the harnesses switched off in harness-prefs.json (nativeSubagentsOff, crates/engine/src/registry.rs:605, :624) and serves them over two new RPCs, GetNativeSubagents and SetNativeSubagents (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).
  • Which chats. Only a chat without a parent runs side-chats-only (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.
  • A run that outruns its row. A run can reach its host before the chat's workspace row, by peer relay for one: the host then claims a row with no parent (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's createChat lands, which records parentChatId either 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.
  • Claude Code. A side-chats-only run launches with --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 tool Agent, while 2.1.310's init frame still lists it under its former name, Task; either name alone removes it.
  • Workflow. Claude Code's Workflow tool 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's Agent,Task leaves 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.
  • Agent guidance. The switch rides the run's Zeron MCP server as 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 its initialize instructions: delegate with create_chats where 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).
  • When it applies. A session parked between turns that started the other way is replaced before its next turn, like one whose CLI was updated in place, with the same guards: never during a turn, never while it holds background work (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).
  • UI. An expanded Claude Code row gets a Delegation section with the switch. The switch moves at once. While the device answers, another flip waits, as in commit credit (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).
  • CI. scripts/ci/test-core.sh:27 now runs the engine's side_chats tests, right after restart_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

  • macOS 27.0, rustc 1.99.0, cargo test --locked: -p zeron-engine --lib --test side_chats --test restart_resume --test e2e gives 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-targets is 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_routing gives 11 passed, -p zeron-rpc --test device_room 13 (1 ignored).
  • New tests, each failing with its fix reverted:
    • 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 with Bash only and a side-chats-only server; a side chat with Bash, Task and Workflow, and a server without the rule; so does a chat whose row its run claimed. It fails at side_chats.rs:992 without 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's Agent,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 lists Bash, Task and Workflow, then only Bash under the flag. With [Feature]: Provider setting to turn off native subagents (Claude Code, Codex) and delegate through side chats #1003's Agent,Task it fails at claude.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's thread/start carries 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 at registry.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 :266 without the UnknownMethod wording (left: "unknown method: GetNativeSubagents"), at :281 when an error hides the row (left: None), and at :285 with Codex back in the gate (left: Some((true, false, "Update Zeron on the selected device to change this."))).
  • Changed tests:
    • 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's config, which now comes from the shared AGENT_TOOLS list (crates/harness/src/codex/mod.rs:128, :1325). Without them it fails at codex.rs:1643 (error: Some("title restriction missing")).
    • old_harness_preferences_default_to_automatic_titles (crates/engine/src/registry.rs:1555): one assert added, an older harness-prefs.json turns nothing off.
    • initialize_and_list_over_jsonrpc (crates/mcp/src/tools.rs:2939): initialize carries the delegation rule exactly when the server's origin is side-chats-only. With initialize ignoring the origin it fails at tools.rs:2957 (left: false, right: true).
  • Live, Claude Code 2.1.310 with Haiku, stream-json in bypass mode, the init frame's tools under each --disallowedTools value. None: Task, ListAgents, SendMessage, TaskStop, Workflow. Agent,Task,Workflow: ListAgents, SendMessage, TaskStop. Agent,Task, Agent or Task alone: those three and Workflow.
  • Live, on a headless engine built from this branch before its last fixes (the Codex gate, the unknown-parent check, the UI guard, the MCP origin), switched over IPC (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.
    • Switch off, a new chat asked for a subagent to run ls. Per ps, its Claude process ran with --disallowedTools=Agent,Task,Workflow, and its Zeron server's env held ZERON_SIDE_CHATS_ONLY=1. Its sessionStarted (journal seq 1) listed none of Agent, Task and Workflow. The agent said its subagent tools were off, called create_chat, and the side chat ran ls.
    • That side chat ran with neither the flag nor the env var. Its sessionStarted listed Task and Workflow, and its server's initialize kept the one-level rule without the delegation rule (the parent's has both).
    • Switch back on, a second turn in the same chat: the engine logged native subagents switched; replacing the parked session, the new process resumed the session without the flag, its sessionStarted (seq 623) listed Task and Workflow, and the agent's Agent call returned pong.
    • A build with [Feature]: Provider setting to turn off native subagents (Claude Code, Codex) and delegate through side chats #1003's Agent,Task and without the parent check, same scenario: switched off, sessionStarted seq 1 lacked Agent and Task but listed Workflow, and its side chat's seq 1 lacked Task.
  • Live again for the screenshots, on this branch's final build: the switch clicked off in the offscreen Shell, then the first and third steps. GetNativeSubagents answered {"off": ["claude-code"], "switchable": ["claude-code"]}. The first turn's Claude process had --disallowedTools=Agent,Task,Workflow in all 74 ps samples, and the agent delegated through create_chat. Switched back on over IPC, the engine logged the replacement, the second turn's new process ran without the flag (33 samples), and its Agent call returned pong.
  • Rebased onto v0.2.124 (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's mock-server tests, zeron-mcp and the UI's settings:: tests, each executable checked to be built from that merge. The one failure, preview's backgrounded_scrubbed_terminal_and_out_of_tree_servers_stay_attributed, fails the same way on v0.2.124 alone, on this Mac.
    • Live on that merged build, the same steps: switched off, the flag was in all 87 ps samples and the agent delegated through a side chat; switched back on, the parked session was replaced, and the next turn's Agent call returned pong.
  • Not run: Codex live (its CLI doesn't start on this Mac); a cross-device run, including one that outruns its row (the local edge can't run here); Windows and Linux; the switch clicked by hand in the packaged app (the UI tests cover the row, and the screenshots' run clicked it in the offscreen Shell).

Known limits

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.md says 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.

Default Switched off
Claude Code's details: Completion, then Delegation with Native subagents on. Its line reads: Off: delegate through side chats instead. Applies to each chat's next turn once its background work is done. The same, with Native subagents off

The live chat's session card: the side chat its first turn delegated to while the switch was off, and the Agent run of its second turn once it was back on.

Session card: Side chats lists ls entry count, done; Subagents shows 1 finished


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

@alexandre-vl

Copy link
Copy Markdown
Contributor Author

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 offline-linux reaching its 45-minute limit while still compiling dependencies; it restored no Rust cache (job). It passed in the three runs before: 34m53s, 41m37s and 8m21s.

The other failures along the way:

A re-run of the failed job would settle it without running the rest again.

@alexandre-vl
alexandre-vl marked this pull request as ready for review October 25, 2026 22:57
@zeronsh
zeronsh Bot force-pushed the subagents/native-subagents-setting branch 2 times, most recently from 7363cce to 684cabe Compare October 26, 2026 22:46
alexandre-vl and others added 5 commits October 26, 2026 22:52
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>
@zeronsh
zeronsh Bot force-pushed the subagents/native-subagents-setting branch from 684cabe to f65c21d Compare October 26, 2026 23:13

@zeronsh zeronsh Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@zeronsh
zeronsh Bot merged commit b8ed9d9 into zeronsh:main Oct 26, 2026
21 of 22 checks passed
zeronsh Bot added a commit to alexandre-vl/zeron that referenced this pull request Oct 27, 2026
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>
zeronsh Bot added a commit to alexandre-vl/zeron that referenced this pull request Oct 27, 2026
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>
zeronsh Bot added a commit that referenced this pull request Oct 27, 2026
* 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>
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.

2 participants