Skip to content

[Go SDK] OpenCode harness has no concurrency guard #438

@santoshkumarradha

Description

@santoshkumarradha

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

  • At most MaxConcurrent opencode subprocesses run simultaneously across concurrent Execute calls on the same provider instance
  • A caller blocked waiting for a slot is unblocked immediately when the context is cancelled (returns context.Canceled)
  • Default behavior (MaxConcurrent not set) limits to 4 concurrent executions
  • Tests pass (go test ./sdk/go/...)
  • Linting passes (make lint)

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingsdk:goGo SDK related

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions