Skip to content
This repository was archived by the owner on Aug 10, 2026. It is now read-only.

feat: decouple worker concurrency from coordinator uniqueness - #229

Merged
pedronauck merged 2 commits into
compozy:mainfrom
richardfogaca:feat/decouple-worker-concurrency
May 29, 2026
Merged

pedronauck merged 2 commits into
compozy:mainfrom
richardfogaca:feat/decouple-worker-concurrency

Conversation

@richardfogaca

@richardfogaca richardfogaca commented May 29, 2026 •

Copy link
Copy Markdown
Contributor

TL;DR - Make the coordinator workspace session budget configurable with one hard-cut setting: max_active_sessions_per_workspace defaults to 5, so a coordinator can run workers alongside itself while coordinator uniqueness remains owned by the daemon singleton.

The problem

A coordinator can never spawn a worker. Every agh spawn from a live coordinator fails:

session: spawn limit exceeded: workspace "..." has 1 active spawned sessions, max_active_per_workspace 1

Root cause: the coordinator's spawn budget capped active managed sessions in the workspace at 1. The coordinator itself fills that slot, so no worker can start.

The fix

Use one public coordinator setting for the active managed session budget:

Setting Meaning Default
max_active_sessions_per_workspace concurrent autonomy-managed sessions in a workspace: coordinator + spawned workers 5

Coordinator uniqueness is not budget-driven. It is enforced by the daemon runtime singleton check for a healthy active coordinator in the workspace. The coordinator is still created through the daemon coordinator path, not through worker spawn.

Changes

File Change
internal/config/autonomy.go keep only MaxActiveSessionsPerWorkspace, default 5, validate it as positive
internal/config/merge.go remove the old max_active_per_workspace coordinator overlay
internal/coordinator/coordinator.go seed the spawn budget from MaxActiveSessionsPerWorkspace
internal/api/core/agent_contracts.go / internal/api/contract/agents.go expose the active session cap in the agent-facing coordinator config payload
openapi/agh.json / web/src/generated/agh-openapi.d.ts refresh the agent coordinator config contract field
docs and tests document the hard-cut setting and cover defaults, validation, overlays, and API payloads

AGH Impact Audit

Surface Impact
Native tools No native tool schema or tool behavior changes.
Extensibility & hooks No hook declarations, extension contracts, or bridge behavior changes.
Workspace data isolation Session cap remains workspace-scoped; no cross-workspace data access changes.
Official AGH skill No bundled skill prompt or skill contract changes.

Testing

  • go test ./internal/config/... ./internal/coordinator/... ./internal/api/... ./internal/daemon/... ./internal/workspace/...
  • go test ./...
  • go run ./cmd/agh-codegen check
  • go build ./...
  • go run github.com/golangci/golangci-lint/v2/cmd/[email protected] run --allow-parallel-runners --timeout 10m ./...

Summary by CodeRabbit

Release Notes

  • Configuration Updates

    • Renamed coordinator session limit configuration field from max_active_per_workspace to max_active_sessions_per_workspace
  • API Updates

    • Coordinator configuration endpoint now returns max_active_sessions_per_workspace instead of max_active_per_workspace
  • Improvements

    • Simplified coordinator session limit validation logic

Review Change Stack

Copilot AI review requested due to automatic review settings May 29, 2026 12:16
@vercel

vercel Bot commented May 29, 2026

Copy link
Copy Markdown

@richardfogaca is attempting to deploy a commit to the Compozy Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented May 29, 2026 •

Copy link
Copy Markdown

Walkthrough

Replaces coordinator config field MaxActivePerWorkspace with MaxActiveSessionsPerWorkspace (default 5). Adds DefaultCoordinatorMaxActiveSessionsPerWorkspace, requires the value be positive during validation, updates TOML overlay handling, API payloads, runtime wiring into SpawnBudget/Lineage, and relevant tests.

Changes

Coordinator Session Cap Configuration

Layer / File(s) Summary
Configuration schema, defaults, and validation
internal/config/autonomy.go
Adds DefaultCoordinatorMaxActiveSessionsPerWorkspace and CoordinatorConfig.MaxActiveSessionsPerWorkspace, initializes it in DefaultCoordinatorConfig(), and updates Validate() to require the value be positive.
Configuration overlay and merge support
internal/config/merge.go
coordinatorOverlay TOML key renamed to max_active_sessions_per_workspace; Apply() assigns dst.MaxActiveSessionsPerWorkspace when present.
Configuration defaults and validation tests
internal/config/autonomy_test.go
Adds tests asserting the new default, validates rejection of non-positive values, and updates config load/overlay tests to use max_active_sessions_per_workspace.
API payload and contract mappings
internal/api/contract/agents.go, internal/api/core/*, internal/api/udsapi/*, internal/api/spec/*
Renames transported JSON field to max_active_sessions_per_workspace, updates contract mapping builders and multiple API tests/specs to use MaxActiveSessionsPerWorkspace.
Runtime wiring and remaining test updates
internal/coordinator/coordinator.go, internal/daemon/*, internal/workspace/*
Lineage/SpawnBudget now source per-workspace cap from cfg.MaxActiveSessionsPerWorkspace; runtime and resolver tests updated accordingly.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main objective: decoupling worker concurrency from coordinator uniqueness, which is the core change across all modified files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@richardfogaca
richardfogaca force-pushed the feat/decouple-worker-concurrency branch from a9d0b73 to f091edd Compare May 29, 2026 12:20

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/config/autonomy_test.go`:
- Around line 49-69: Restructure TestCoordinatorConfigValidatesMaxActiveSessions
into three t.Run subtests (e.g., "Should validate default", "Should error when
max_active_sessions_per_workspace too low", "Should validate higher") and call
t.Parallel() inside each subtest; locate the logic around
DefaultCoordinatorConfig(), modifications to MaxActiveSessionsPerWorkspace and
Validate("autonomy.coordinator", nil). For the invalid case assert the error is
non-nil AND that the error message contains the expected text (e.g.,
"max_active_sessions_per_workspace" or the specific validation phrase) using an
ErrorContains-style check rather than only err != nil; keep the valid-case
checks identical but wrapped in their own subtests.

In `@internal/config/autonomy.go`:
- Around line 25-29: Replace the hardcoded literal 5 used by
DefaultCoordinatorMaxActiveSessionsPerWorkspace with a value retrieved from the
project's configuration/environment system: read the int option (e.g., via the
existing config package or environment variable like
COORDINATOR_MAX_ACTIVE_SESSIONS_PER_WORKSPACE) and fall back to 5 if unset;
update the initializer for DefaultCoordinatorMaxActiveSessionsPerWorkspace to
call that config lookup instead of embedding the literal so the default is
driven by the config layer.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5bfa7d76-6f27-4cf1-9c4c-cd0ed3e4803f

📥 Commits

Reviewing files that changed from the base of the PR and between d3b24f9 and a9d0b73.

⛔ Files ignored due to path filters (1)
  • packages/site/content/runtime/core/configuration/config-toml.mdx is excluded by !**/*.mdx
📒 Files selected for processing (4)
  • internal/config/autonomy.go
  • internal/config/autonomy_test.go
  • internal/config/merge.go
  • internal/coordinator/coordinator.go

Comment thread internal/config/autonomy_test.go Outdated
Comment thread internal/config/autonomy.go Outdated
@richardfogaca
richardfogaca force-pushed the feat/decouple-worker-concurrency branch from f091edd to 6d360f4 Compare May 29, 2026 12:28

@pedronauck pedronauck left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

there are some conflicts to solve @richardfogaca

@pedronauck pedronauck changed the title feat(autonomy): decouple worker concurrency from coordinator uniqueness feat: decouple worker concurrency from coordinator uniqueness May 29, 2026
The coordinator's spawn budget MaxActivePerWorkspace was seeded from autonomy.coordinator.max_active_per_workspace, which is pinned to 1 to preserve coordinator uniqueness. That made the coordinator consume the workspace's only active-session slot, so it could never spawn a worker, blocking autonomous worker execution.

Add autonomy.coordinator.max_active_sessions_per_workspace with default 5, and feed the spawn budget from it. max_active_per_workspace stays pinned at 1; coordinator uniqueness remains enforced by the daemon singleton check.
@richardfogaca
richardfogaca force-pushed the feat/decouple-worker-concurrency branch from 6d360f4 to 50e3134 Compare May 29, 2026 15:34
@richardfogaca

Copy link
Copy Markdown
Contributor Author

conflicts solved @pedronauck PTAL when you get a chance, thanks

@pedronauck pedronauck left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The fix is functionally correct and solves a real, confirmed blocker — the coordinator counts itself in validateSpawnCaps (internal/session/spawn.go:284-307, since its normalized lineage sets RootSessionID = coordinator.ID via store/session_lineage.go:71-73), so with the budget pinned to 1 no worker can ever spawn. Repro and root cause check out, and the fix is targeted at the right line (the budget flows through spawnLineage → effectiveSpawnBudget, which doesn't touch MaxActivePerWorkspace).

That said, I don't think this is the right shape for the change, and there are gaps. Requesting changes on the following:

1. The core rationale is inaccurate

The PR argues:

Simply allowing max_active_per_workspace > 1 would instead permit multiple coordinators, which is why a separate field is the correct shape.

This isn't true. Coordinator uniqueness is enforced solely by the daemon singleton check — activeCoordinator + coordinator.HealthySession under a mutex (internal/daemon/coordinator_runtime.go:296-307,404-416). That check never consults the spawn budget. The coordinator is created via Create (startCoordinatorSession), not via Spawn, so max_active_per_workspace has never gated coordinator creation. Bumping it would only ever allow more workers — which is exactly the goal here. The premise that a second field is needed to protect uniqueness doesn't hold.

2. This leaves max_active_per_workspace as a vestigial field

After this change, max_active_per_workspace is still validated to be exactly 1 (internal/config/autonomy.go:161) and still exposed over the API, but it no longer feeds any runtime enforcement. That's a validated-but-no-op knob. Per the repo's "Greenfield Alpha — Zero Legacy Tolerance" rules (no dual fields, hard cuts, delete obsolete code instead of bridging), the cleaner fix is to repurpose the existing field to mean the session cap (default 5, drop the "must be 1" pin) and lean on the daemon singleton for uniqueness — rather than adding a parallel field and advertising "Fully backward compatible."

3. Near-duplicate naming is a footgun

max_active_per_workspace (coordinators, =1) vs max_active_sessions_per_workspace (all managed sessions, =5) differ by a single word. This is very easy to confuse for both operators and agents. If we do keep two fields, they need names that don't collide on read.

4. Partial-surface gap (agent-manageability)

The new knob materially governs worker concurrency, but it's not plumbed into CoordinatorConfigPayload (internal/api/core/agent_contracts.go:16-26), which is served at GET /api/agent/coordinator/config over both HTTP and UDS (httpapi/routes.go:143, udsapi/routes.go:166) and is part of the agent Situation Surface. As-is, an agent inspecting its coordinator config sees the now-meaningless max_active_per_workspace but not the field that actually controls its worker budget. This violates "No partial-surface completions" / "Agent-manageable by default." Please plumb the new field through the contract payload (and regenerate the OpenAPI/TS if the contract DTO changes).

5. Missing AGH Impact Audit

The PR body is missing the required four-row AGH Impact Audit (native tools / extensibility & hooks / workspace data isolation / official AGH skill). Please add it.

Suggested direction

I'd prefer the hard-cut version: rename/repurpose to a single max_active_sessions_per_workspace (default 5), delete the must be 1 validation, document that coordinator uniqueness is owned by the daemon singleton, and expose the field in the agent-facing payload. If you want to keep an explicit uniqueness invariant as defense-in-depth, that's fine — but then fix the rationale, keep it out of the spawn-budget seed entirely, and resolve the naming collision.

Also: Verify (fmt, lint, unit, build) was still pending at review time — let's get that green before merge.

@richardfogaca

richardfogaca commented May 29, 2026 •

Copy link
Copy Markdown
Contributor Author

thanks for the review @pedronauck, I went with the hard-cut direction you suggested:

  1. Removed the old coordinator max_active_per_workspace config field instead of keeping two near-duplicate knobs.
  2. Kept a single max_active_sessions_per_workspace setting, defaulting to 5, and validated it as the workspace-scoped active managed session cap.
  3. Updated the rationale in the PR body: coordinator uniqueness is enforced by the daemon singleton check, not by the spawn budget.
  4. Plumbed max_active_sessions_per_workspace through the agent-facing coordinator config payload and refreshed the OpenAPI / generated TS surfaces.
  5. Updated docs/tests and added the required AGH Impact Audit to the PR body.

Validation passed locally: focused config/coordinator/API/daemon/workspace tests, go test ./..., go build ./..., codegen check, and Go lint.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
internal/api/spec/spec_test.go (1)

1025-1033: ⚡ Quick win

Assert the legacy coordinator property is absent.

This checks that max_active_sessions_per_workspace is required, but it would still pass if the generated schema accidentally kept the old max_active_per_workspace alongside it. Adding an explicit absence check will catch partial rename regressions in the OpenAPI contract.

Suggested test hardening
 				assertRequired(
 					t,
 					configSchema,
 					"enabled",
 					"agent_name",
 					"default_ttl_seconds",
 					"max_children",
 					"max_active_sessions_per_workspace",
 					"source",
 				)
+				assertPropertyAbsent(t, configSchema, "max_active_per_workspace")
 				assertEnumValues(t, propertySchema(t, configSchema, "source"), "workspace", "global", "default")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/api/spec/spec_test.go` around lines 1025 - 1033, The test currently
asserts required fields via assertRequired but doesn't ensure the old property
name is removed; add an explicit absence assertion for the legacy
"max_active_per_workspace" property (e.g. use assert.NotContains or
require.NotContains) immediately after the assertRequired call to verify
configSchema does NOT include "max_active_per_workspace" so a partial rename
won't slip through.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@internal/api/spec/spec_test.go`:
- Around line 1025-1033: The test currently asserts required fields via
assertRequired but doesn't ensure the old property name is removed; add an
explicit absence assertion for the legacy "max_active_per_workspace" property
(e.g. use assert.NotContains or require.NotContains) immediately after the
assertRequired call to verify configSchema does NOT include
"max_active_per_workspace" so a partial rename won't slip through.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a6882724-e82f-4df5-9f36-d294e6001b1d

📥 Commits

Reviewing files that changed from the base of the PR and between 50e3134 and d994ecd.

⛔ Files ignored due to path filters (5)
  • openapi/agh.json is excluded by !**/*.json
  • packages/site/content/runtime/core/autonomy/coordinator.mdx is excluded by !**/*.mdx
  • packages/site/content/runtime/core/autonomy/execution-profiles.mdx is excluded by !**/*.mdx
  • packages/site/content/runtime/core/configuration/config-toml.mdx is excluded by !**/*.mdx
  • web/src/generated/agh-openapi.d.ts is excluded by !**/generated/**
📒 Files selected for processing (12)
  • internal/api/contract/agents.go
  • internal/api/core/agent_channels_internal_test.go
  • internal/api/core/agent_contracts.go
  • internal/api/core/agent_contracts_test.go
  • internal/api/spec/spec_test.go
  • internal/api/udsapi/agent_channels_test.go
  • internal/config/autonomy.go
  • internal/config/autonomy_test.go
  • internal/config/merge.go
  • internal/coordinator/coordinator.go
  • internal/daemon/coordinator_runtime_test.go
  • internal/workspace/resolver_test.go
💤 Files with no reviewable changes (1)
  • internal/config/merge.go
✅ Files skipped from review due to trivial changes (2)
  • internal/coordinator/coordinator.go
  • internal/workspace/resolver_test.go

@pedronauck
pedronauck merged commit 7cc2e89 into compozy:main May 29, 2026
6 of 7 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants