From a53fe9a03ea9ffde90c32d3fee1141fc4d7aaeb9 Mon Sep 17 00:00:00 2001
From: SivanCola <32437197+SivanCola@users.noreply.github.com>
Date: Sat, 18 Jul 2026 14:12:33 +0800
Subject: [PATCH 1/3] feat(subagents): add profile-aware parallel fleets
Problem:
Custom subagent profiles could only be run serially through skill execution, making multi-document workloads unnecessarily slow and leaving concurrent writers without one session-wide safety boundary.
Root cause:
Task execution had no runtime profile selector or shared fleet scheduler, and write coordination was split across entry points without atomic parent reservations or enforceable path claims.
Fix:
- add profile-aware task and fleet execution with session-wide total/writer concurrency limits
- normalize and enforce write path claims, including sandboxed Bash roots and parent write reservations
- make background tasks queue asynchronously and keep parallel_tasks on the shared scheduler
- expose concurrency and read-only profile controls in Desktop and document the contracts
Verification:
- go test ./...
- cd desktop && go test ./...
- go test -race ./internal/agent ./internal/boot ./internal/config ./internal/skill
- cd desktop/frontend && pnpm typecheck && pnpm test:typecheck && pnpm test
- ./scripts/cache-guard.sh
---
desktop/app.go | 6 +-
.../settings-refresh-snapshot.test.tsx | 2 +-
.../frontend/src/components/SettingsPanel.tsx | 42 +-
.../src/components/SubagentsPanel.tsx | 23 +
desktop/frontend/src/lib/bridge.ts | 16 +-
desktop/frontend/src/lib/types.ts | 4 +
desktop/frontend/src/locales/en.ts | 8 +
desktop/frontend/src/locales/zh-TW.ts | 8 +
desktop/frontend/src/locales/zh.ts | 8 +
desktop/settings_app.go | 69 ++-
desktop/subagents_app.go | 5 +
desktop/subagents_app_test.go | 49 ++-
docs/GUIDE.md | 3 +
docs/GUIDE.zh-CN.md | 3 +
docs/SPEC.md | 17 +-
docs/SUBAGENT_PROFILES.md | 35 +-
docs/SUBAGENT_PROFILES.zh-CN.md | 31 +-
docs/TOOL_CONTRACT.md | 2 +-
docs/TOOL_CONTRACT.zh-CN.md | 2 +-
internal/agent/agent.go | 74 +++-
internal/agent/fleet.go | 353 ++++++++++++++++
internal/agent/fleet_test.go | 124 ++++++
internal/agent/parallel_tasks.go | 19 +
internal/agent/path_bound_tools.go | 211 ++++++++++
internal/agent/path_bound_tools_test.go | 295 +++++++++++++
internal/agent/profile_spec.go | 184 ++++++++
internal/agent/profile_spec_test.go | 77 ++++
internal/agent/scheduler.go | 309 ++++++++++++++
internal/agent/scheduler_test.go | 115 +++++
internal/agent/subagent_registry_test.go | 7 +-
internal/agent/task.go | 397 +++++++++++++++---
internal/agent/task_background_queue_test.go | 85 ++++
internal/agent/task_profile_test.go | 129 ++++++
internal/agent/write_claims.go | 270 ++++++++++++
internal/agent/write_claims_test.go | 123 ++++++
internal/boot/boot.go | 101 ++++-
internal/boot/boot_test.go | 5 +-
internal/cli/subagent.go | 7 +-
internal/cli/subagent_test.go | 25 +-
internal/config/config.go | 26 +-
internal/config/render.go | 10 +
internal/doctor/skill_health.go | 1 +
internal/skill/profile.go | 2 +-
internal/skill/tools.go | 15 +-
internal/tool/builtin/confine.go | 23 +
reasonix.example.toml | 2 +
46 files changed, 3191 insertions(+), 131 deletions(-)
create mode 100644 internal/agent/fleet.go
create mode 100644 internal/agent/fleet_test.go
create mode 100644 internal/agent/path_bound_tools.go
create mode 100644 internal/agent/path_bound_tools_test.go
create mode 100644 internal/agent/profile_spec.go
create mode 100644 internal/agent/profile_spec_test.go
create mode 100644 internal/agent/scheduler.go
create mode 100644 internal/agent/scheduler_test.go
create mode 100644 internal/agent/task_background_queue_test.go
create mode 100644 internal/agent/task_profile_test.go
create mode 100644 internal/agent/write_claims.go
create mode 100644 internal/agent/write_claims_test.go
diff --git a/desktop/app.go b/desktop/app.go
index cd0101a1be..f4d2d34b60 100644
--- a/desktop/app.go
+++ b/desktop/app.go
@@ -6076,7 +6076,10 @@ type SkillView struct {
Model string `json:"model,omitempty"`
Effort string `json:"effort,omitempty"`
AllowedTools []string `json:"allowedTools,omitempty"`
- Color string `json:"color,omitempty"`
+ // ReadOnly mirrors frontmatter read-only; omitted/false keeps the legacy
+ // writable default for older profiles.
+ ReadOnly bool `json:"readOnly,omitempty"`
+ Color string `json:"color,omitempty"`
// Invocation is the user-facing slash name; InvocationMode preserves the
// frontmatter policy used by the subagent profile editor.
Invocation string `json:"invocation,omitempty"`
@@ -6456,6 +6459,7 @@ func (a *App) SkillsSettings() SkillsSettingsView {
Model: s.Model,
Effort: s.Effort,
AllowedTools: append([]string{}, s.AllowedTools...),
+ ReadOnly: s.ReadOnly,
Color: s.Color,
Invocation: "/" + s.SlashName(),
InvocationMode: s.Invocation,
diff --git a/desktop/frontend/src/__tests__/settings-refresh-snapshot.test.tsx b/desktop/frontend/src/__tests__/settings-refresh-snapshot.test.tsx
index 52b46fb930..9d1372dd95 100644
--- a/desktop/frontend/src/__tests__/settings-refresh-snapshot.test.tsx
+++ b/desktop/frontend/src/__tests__/settings-refresh-snapshot.test.tsx
@@ -78,7 +78,7 @@ function baseSettings(displayMode: "standard" | "compact" = "standard"): Setting
permissions: { mode: "ask", allow: [], ask: [], deny: [] },
sandbox: { bash: "enforce", network: false, workspaceRoot: "", allowWrite: [], effectiveWorkspaceRoot: "/work", effectiveWriteRoots: ["/work"], shell: "auto" },
network: { proxyMode: "auto", proxyUrl: "", noProxy: "", proxy: { type: "socks5", server: "", port: 0, username: "", password: "" } },
- agent: { temperature: 0, maxSteps: 0, plannerMaxSteps: 0, maxSubagentDepth: 2, systemPrompt: "", coldResumePrune: true, reasoningLanguage: "auto" },
+ agent: { temperature: 0, maxSteps: 0, plannerMaxSteps: 0, maxSubagentDepth: 2, maxSubagentConcurrency: 6, maxParallelWriters: 3, systemPrompt: "", coldResumePrune: true, reasoningLanguage: "auto" },
bot: {
enabled: false,
model: "",
diff --git a/desktop/frontend/src/components/SettingsPanel.tsx b/desktop/frontend/src/components/SettingsPanel.tsx
index 7a9e820452..d2f13401ff 100644
--- a/desktop/frontend/src/components/SettingsPanel.tsx
+++ b/desktop/frontend/src/components/SettingsPanel.tsx
@@ -1246,7 +1246,7 @@ function normalizeSettingsView(view: SettingsView | null | undefined): SettingsV
noProxy: "",
proxy: { type: "socks5", server: "", port: 0, username: "", password: "" },
};
- const agent = view.agent ?? { temperature: 0, maxSteps: 0, plannerMaxSteps: 0, maxSubagentDepth: 2, systemPrompt: "", coldResumePrune: true, reasoningLanguage: "auto" };
+ const agent = view.agent ?? { temperature: 0, maxSteps: 0, plannerMaxSteps: 0, maxSubagentDepth: 2, maxSubagentConcurrency: 6, maxParallelWriters: 3, systemPrompt: "", coldResumePrune: true, reasoningLanguage: "auto" };
agent.plannerMaxSteps = Number.isFinite(agent.plannerMaxSteps) ? Math.max(0, Math.trunc(agent.plannerMaxSteps)) : 0;
agent.maxSteps = Number.isFinite(agent.maxSteps) ? Math.max(0, Math.trunc(agent.maxSteps)) : 0;
agent.maxSubagentDepth = Number.isFinite(agent.maxSubagentDepth) && agent.maxSubagentDepth <= 1 ? 1 : 2;
@@ -3950,8 +3950,14 @@ function ModelsSection({ s, busy, apply, backgroundApply }: ModelsSectionProps)
: !providerIsConfigured(defaultProviderView)
? t("settings.modelNeedsKey", { provider: modelProviderLabel(defaultProvider, defaultProviderView, t) })
: "";
- const agent = s.agent ?? { temperature: 0, maxSteps: 0, plannerMaxSteps: 0, maxSubagentDepth: 2, systemPrompt: "", coldResumePrune: true, reasoningLanguage: "auto" };
+ const agent = s.agent ?? { temperature: 0, maxSteps: 0, plannerMaxSteps: 0, maxSubagentDepth: 2, maxSubagentConcurrency: 6, maxParallelWriters: 3, systemPrompt: "", coldResumePrune: true, reasoningLanguage: "auto" };
const subagentDepth = Number.isFinite(agent.maxSubagentDepth) && agent.maxSubagentDepth <= 1 ? 1 : 2;
+ const subagentConcurrency = Number.isFinite(agent.maxSubagentConcurrency) && agent.maxSubagentConcurrency > 0
+ ? Math.max(1, Math.min(32, Math.floor(agent.maxSubagentConcurrency)))
+ : 6;
+ const parallelWriters = Number.isFinite(agent.maxParallelWriters) && agent.maxParallelWriters > 0
+ ? Math.max(1, Math.min(subagentConcurrency, Math.floor(agent.maxParallelWriters)))
+ : Math.min(3, subagentConcurrency);
useEffect(() => {
if (subtab !== "usage") return;
@@ -4077,6 +4083,38 @@ function ModelsSection({ s, busy, apply, backgroundApply }: ModelsSectionProps)
+