feat(desktop): cap OpenClaw agent parallelism at 5 (#4019)

OpenClaw connects to a single shared Gateway daemon. Spawning the
default 10 ACP workers per agent is both resource-expensive and
architecturally wrong — each worker opens a separate gateway connection.
Tyler's ruling: cap at 5, lower if needed.

## Contract

Store the requested value (1–32) verbatim at every persistence and wire
boundary. Apply `effective = min(requested, harness_cap)` only at the
four enforcement points:

| Boundary | Implementation |
|---|---|
| Local spawn | `BUZZ_ACP_AGENTS` env var in child `Command` |
| Remote deploy | `launch.policy_env["BUZZ_ACP_AGENTS"]` + legacy
`parallelism` field |
| Restart badge | `SpawnConfigSnapshot.parallelism` stores effective
value; the diff surface displays what actually runs |
| UI copy | Amber hint when requested > cap; no `max` attribute, no
save-path clamp |

`BUZZ_ACP_AGENTS` is added to `RESERVED_ENV_KEYS` — the Desktop resolves
the effective value into `policy_env`; a user-supplied override in `env`
would bypass the cap and is silently stripped.

## Changes

**`managed_agents/parallelism.rs`** (new) — policy core:
- `OPENCLAW_MAX_PARALLELISM = 5`
- `harness_max_parallelism(command)` — keyed on
`normalize_command_identity` so path prefixes, `.exe` suffixes, and
other cosmetic differences are ignored
- `effective_parallelism(command, value)` — identity for uncapped
harnesses
- `acp_agents_value(command, parallelism)` — `env("BUZZ_ACP_AGENTS", …)`
helper

**`runtime.rs`** — spawn clamp: `BUZZ_ACP_AGENTS =
acp_agents_value(effective_command, record.parallelism)`

**`agents_deploy.rs`** — deploy egress clamp: `build_deploy_payload`
resolves `effective_parallelism` once from `descriptor.command`; both
`launch.policy_env["BUZZ_ACP_AGENTS"]` and the legacy top-level
`parallelism` field use that value — the two are always consistent
regardless of stale `record.agent_command` pins

**`spawn_snapshot.rs`** — `from_inputs` stores
`effective_parallelism(&descriptor.command, record.parallelism)` in the
`parallelism` field. Over-cap edits that don't change the pool (e.g. 10
→ 8, both clamp to 5 on OpenClaw) produce equal snapshots; cap crossings
(8 → 3) produce different snapshots.

**`AcpRuntimeCatalogEntry.max_parallelism: Option<u32>`** — derived from
the static definition command, not the probed `entry.command` (which may
be `null` for unavailable entries), so unavailable OpenClaw entries
still carry the cap. Propagated through all four catalog constructors
(builtin discovery, preset catalog construction, custom discovery,
custom-save response), IPC types
(`RawAcpRuntimeCatalogEntry.max_parallelism`), and the frontend catalog
type.

**UI** — `EditAgentAdvancedFields` and `PersonaAdvancedFields` show an
amber hint when `selectedRuntime.maxParallelism` is set and the current
value exceeds it. Cap and label come from the catalog entry — no
hardcoded 5 in TS. No `max` attribute on inputs; the input stays
`type="text"` with 1–32 copy.

**Docs** — `docs/remote-agents.md`: `BUZZ_ACP_AGENTS` moved from the
deliberately-non-reserved section to reserved; new contract documented.
`desktop/src/features/agents/AGENTS.md`: command-keyed execution policy
documented as the sanctioned second metadata source feeding the catalog
projection.

## Tests

**Rust** (`parallelism.rs`):
- `policy_table` — `harness_max_parallelism` and `effective_parallelism`
across all openclaw variants and uncapped harnesses
- `acp_agents_value_openclaw_above_cap_is_capped` — spawn-env seam
- `override_direction_*` — both override directions (openclaw runtime +
goose override; goose runtime + openclaw override)
- `summary_persona_inherited_*` — live persona wins over stale
`agent_command`
- `snapshot_export_carries_requested_definition_parallelism` — requested
value travels wire/sync unchanged

**Rust** (`spawn_snapshot/tests.rs`):
- `openclaw_above_cap_parallelism_snapshots_equal` — stored 10 vs 8,
both clamp to 5 → snapshots equal
- `openclaw_cap_crossing_parallelism_snapshots_differ` — 8 (clamps to 5)
vs 3 → snapshots differ

**Rust** (`discovery/presets.rs`):
- `openclaw_preset_unavailable_carries_max_parallelism` /
`openclaw_preset_available_carries_max_parallelism` — catalog metadata
present with `command: null` and with a resolved path

**Rust** (`agents_deploy.rs`):
- `launch_block_openclaw_over_cap_policy_env_is_capped` — direct
`launch.policy_env` seam
-
`deploy_payload_json_stale_goose_record_live_openclaw_descriptor_both_capped`
— stale `record.agent_command=goose`, live descriptor=openclaw: both
fields cap to 5
-
`deploy_payload_json_stale_openclaw_record_live_goose_descriptor_both_uncapped`
— stale `record.agent_command=openclaw`, live descriptor=goose: both
fields pass through requested
- `deploy_payload_json_explicit_openclaw_override_both_capped` —
explicit `agent_command_override=openclaw`: both fields cap to 5

**Rust** (`persona_events/stale_pin_tests.rs`):
- `apply_persona_snapshot_goose_to_custom_harness_drops_stale_goose_pin`
— custom-direction stale-pin drop (builtin pin → loaded custom harness
via `update_loaded_harness_registry`)

**TypeScript** (`agentParallelism.test.mjs`):
- `parallelismCapHint` — at/below cap (null), above cap (hint includes
label and cap value), singular form for cap=1, uncapped harness (null)

**TypeScript** (`tauri.test.mjs`):
- `fromRawAcpRuntimeCatalogEntry` round-trips `max_parallelism` →
`maxParallelism`; absent when `undefined`

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
This commit is contained in:
Will Pfleger
2026-08-05 16:09:14 -04:00
committed by GitHub
parent 6df7eba24d
commit 6c40ce394f
23 changed files with 945 additions and 71 deletions

View File

@@ -543,14 +543,17 @@ mis-tiered (below):
`BUZZ_ACP_MAX_TURN_DURATION`, `BUZZ_ACP_AGENTS` — resolved by the desktop
from the record's `system_prompt` / `idle_timeout_seconds` /
`max_turn_duration_seconds` / `parallelism` (each omitted when null,
matching the local spawn's conditional emission). These are **tier-1 by
local fact, not by choice**: the local spawn writes them before the user
env layer (`runtime.rs:716-729,763` vs `:860`) and none is in
`RESERVED_ENV_KEYS`, so a power user's env override beats them today. A
provider that independently mapped the top-level payload copies after
`launch.env` would invert that — the structured field silently defeating
an override that works locally — which is why the provider MUST NOT remap
them (§Entrypoint mapping table).
matching the local spawn's conditional emission). `BUZZ_ACP_AGENTS` is
the **effective** parallelism: `min(record.parallelism, harness_cap)`
where the cap is harness-specific (e.g. OpenClaw is capped at 5). These
are **tier-1 control-plane** keys: `BUZZ_ACP_AGENTS` is in
`RESERVED_ENV_KEYS` (`env_vars.rs`) so the desktop-resolved effective
value cannot be overridden by a definition env var; the others are
tier-1 by local fact (written before the user env layer). A provider
that independently mapped the top-level payload copies after `launch.env`
would invert the precedence for those remaining keys — the structured
field silently defeating an override that works locally — which is why
the provider MUST NOT remap them (§Entrypoint mapping table).
`BUZZ_ACP_DEDUP` and `BUZZ_ACP_MULTIPLE_EVENT_HANDLING` are **deliberately
unset**: the local spawn writes `queue`/`steer` (`runtime.rs:730-731`), and
@@ -564,11 +567,17 @@ process to sweep.
**Environment precedence (normative) — three tiers, later wins:**
1. **Overridable behavior defaults** — `launch.policy_env`. These keys are
deliberately non-reserved (`env_vars.rs:54-57` says so outright: power
users may bypass the dedicated UI fields), and locally the user env is
written after them (`runtime.rs:860` and its comment). A policy-wins
order here would make remote agents ignore overrides local agents honor.
1. **Overridable behavior defaults** — `launch.policy_env`. Most keys here
are deliberately non-reserved (`env_vars.rs` documents the narrow set
that IS reserved): power users may bypass the dedicated UI fields for
system prompt, model, idle timeout, etc. Locally the user env is written
after them (`runtime.rs:860` and its comment). A policy-wins order here
would make remote agents ignore overrides local agents honor.
**Exception — `BUZZ_ACP_AGENTS`:** this key IS reserved
(`env_vars.rs:RESERVED_ENV_KEYS`) so the desktop-controlled effective
parallelism (applying any per-harness cap) cannot be bypassed by a
user-supplied definition env var. The reserved-key strip removes any
user copy before serialization, so the tier-1 value survives.
2. **User/layered env** — `launch.env`. User `env_vars` need no separate
slot: the descriptor's layering already merged them (global < persona <
agent), so a provider applies `launch.env` and MUST NOT re-merge the
@@ -1081,7 +1090,7 @@ individually:
| `launch.args` | `BUZZ_ACP_AGENT_ARGS`, comma-joined |
| `launch.env`, `launch.policy_env` | verbatim, at their precedence tiers |
| generation token (§K8s Secrets) | `BUZZ_MANAGED_AGENT_START_NONCE` — the lifecycle-frame correlator and the Secret generation are one identity (§Launch data tier 3) |
| `system_prompt`, `idle_timeout_seconds`, `max_turn_duration_seconds`, `parallelism` | **not mapped by the provider** — the desktop resolves these into `launch.policy_env` (`BUZZ_ACP_SYSTEM_PROMPT`, `BUZZ_ACP_IDLE_TIMEOUT`, `BUZZ_ACP_MAX_TURN_DURATION`, `BUZZ_ACP_AGENTS`), because they are tier-1 behavior knobs: locally they are written *before* the user env layer and none is reserved (`runtime.rs:716-729,763` vs `:860`; `env_vars.rs:54-57`), so user env beats them. A provider that mapped the top-level copies after `launch.env` would silently defeat an override that works locally. The top-level fields remain as display/bookkeeping inputs only |
| `system_prompt`, `idle_timeout_seconds`, `max_turn_duration_seconds`, `parallelism` | **not mapped by the provider** — the desktop resolves these into `launch.policy_env` (`BUZZ_ACP_SYSTEM_PROMPT`, `BUZZ_ACP_IDLE_TIMEOUT`, `BUZZ_ACP_MAX_TURN_DURATION`, `BUZZ_ACP_AGENTS`). `BUZZ_ACP_AGENTS` carries the **effective** parallelism (`min(record.parallelism, harness_cap)`), is reserved (`env_vars.rs:RESERVED_ENV_KEYS`), and cannot be overridden by user env. The remaining knobs are tier-1 by local fact (written before user env); a provider that mapped the top-level copies after `launch.env` would silently defeat local overrides. The top-level fields remain as display/bookkeeping inputs only |
| `turn_timeout_seconds` | not mapped — deprecated upstream and ignored; the local spawn also does not emit it |
| `respond_to` | `BUZZ_ACP_RESPOND_TO` |
| `respond_to_allowlist` | `BUZZ_ACP_RESPOND_TO_ALLOWLIST`, comma-joined |