Summary
OpenCodeProvider.Execute in the Go SDK harness spawns a new opencode subprocess on every call with no concurrency cap, allowing bursty callers to launch unbounded parallel processes and trigger rate-limit flapping.
Context
harness/opencode.go:29 calls RunCLI immediately for every Execute invocation. A fan-out reasoner or a parallel workflow with 20 branches will launch 20 concurrent opencode processes, each establishing its own LLM API session. This saturates the backend's rate limits, causes 429 cascades, and consumes system resources (file descriptors, process slots) proportionally to fan-out width. The Python SDK's OpenCode provider uses a class-level semaphore to cap concurrency — the Go SDK has no equivalent.
Scope
In Scope
- Add a
MaxConcurrent int field on OpenCodeProvider with a default value (e.g. 4, configurable via OpenCodeConfig).
- Use a buffered channel or
golang.org/x/sync/semaphore to gate entry into RunCLI.
- If
MaxConcurrent is 0 or negative, use the default (do not disable limiting entirely without an explicit NoLimit sentinel).
- Callers blocked on the semaphore should respect context cancellation.
Out of Scope
- Adding a global semaphore across multiple
OpenCodeProvider instances — per-instance is sufficient.
- Changing the
opencode CLI invocation or argument handling.
- Adding retry logic for 429 responses — that is a separate concern.
Files
sdk/go/harness/opencode.go:29 — add semaphore acquisition before RunCLI, release in defer
sdk/go/harness/opencode.go — add MaxConcurrent int to OpenCodeProvider struct and OpenCodeConfig; initialize semaphore in constructor
sdk/go/harness/opencode_test.go — test: with MaxConcurrent=2, a burst of 5 concurrent Execute calls does not launch more than 2 subprocesses simultaneously; context cancellation unblocks a waiting caller
Acceptance Criteria
Notes for Contributors
Severity: MEDIUM
golang.org/x/sync/semaphore is cleaner than a buffered channel for this use case because it supports weighted acquisition (useful if future work adds priority levels). Check if golang.org/x/sync is already a dependency in sdk/go/go.mod; if not, a buffered channel semaphore is acceptable to avoid adding a new dependency.
Summary
OpenCodeProvider.Executein the Go SDK harness spawns a newopencodesubprocess on every call with no concurrency cap, allowing bursty callers to launch unbounded parallel processes and trigger rate-limit flapping.Context
harness/opencode.go:29callsRunCLIimmediately for everyExecuteinvocation. A fan-out reasoner or a parallel workflow with 20 branches will launch 20 concurrentopencodeprocesses, each establishing its own LLM API session. This saturates the backend's rate limits, causes 429 cascades, and consumes system resources (file descriptors, process slots) proportionally to fan-out width. The Python SDK's OpenCode provider uses a class-level semaphore to cap concurrency — the Go SDK has no equivalent.Scope
In Scope
MaxConcurrent intfield onOpenCodeProviderwith a default value (e.g. 4, configurable viaOpenCodeConfig).golang.org/x/sync/semaphoreto gate entry intoRunCLI.MaxConcurrentis 0 or negative, use the default (do not disable limiting entirely without an explicitNoLimitsentinel).Out of Scope
OpenCodeProviderinstances — per-instance is sufficient.opencodeCLI invocation or argument handling.Files
sdk/go/harness/opencode.go:29— add semaphore acquisition beforeRunCLI, release in defersdk/go/harness/opencode.go— addMaxConcurrent inttoOpenCodeProviderstruct andOpenCodeConfig; initialize semaphore in constructorsdk/go/harness/opencode_test.go— test: withMaxConcurrent=2, a burst of 5 concurrentExecutecalls does not launch more than 2 subprocesses simultaneously; context cancellation unblocks a waiting callerAcceptance Criteria
MaxConcurrentopencodesubprocesses run simultaneously across concurrentExecutecalls on the same provider instancecontext.Canceled)MaxConcurrentnot set) limits to 4 concurrent executionsgo test ./sdk/go/...)make lint)Notes for Contributors
Severity: MEDIUM
golang.org/x/sync/semaphoreis cleaner than a buffered channel for this use case because it supports weighted acquisition (useful if future work adds priority levels). Check ifgolang.org/x/syncis already a dependency insdk/go/go.mod; if not, a buffered channel semaphore is acceptable to avoid adding a new dependency.