Skip to content

fix(subagents,llm): enforce process-wide child concurrency + configurable stream idle watchdog - #166

Merged
jkyberneees merged 1 commit into
mainfrom
fix/subagent-concurrency-llm-retry
Aug 31, 2026
Merged

fix(subagents,llm): enforce process-wide child concurrency + configurable stream idle watchdog#166
jkyberneees merged 1 commit into
mainfrom
fix/subagent-concurrency-llm-retry

Conversation

@jkyberneees

Copy link
Copy Markdown
Contributor

Summary

Implements CONCURRENCY_LLM_RESILIENCE_PLAN.md (in-tree): fixes sub-agent provider-concurrency overrun and makes LLM stream failures gracefully survivable. Both issues were observed live (z.ai 429 saturation with 8-subagent swarms; llm: stream idle for over 1m0s without an event aborting turns).

M1 — Process-wide sub-agent concurrency limiter

  • The child semaphore was created per delegate_tasks call — sibling tool calls in one parallel batch (or concurrent serve sessions) each got their own cap, allowing up to N×subagent.max_concurrency child streams against an account-wide provider plan.
  • Now: one process-wide limiter (sharedChildSem, sync.Once) wired by builtinTools into every delegate_tasks instance — all calls and all serve sessions share a single subagent.max_concurrency bound. Entry points need no changes; children (depth-capped) keep their own process limit.
  • Call() waits via per-call WaitGroup — the old cap(sem) drain would permanently consume shared slots and deadlock later calls.
  • Defensive floor: maxConcurrency < 1 normalizes to 1 (an unbuffered semaphore would deadlock the acquire-before-spawn loop).
  • New telemetry: subagent_concurrency_wait runtime event when a task visibly queues on the limiter (threshold 5s), so the Web UI can show backpressure.
  • runTaskFn test seam (nil = the real runTask).

M2 — LLM stream resilience

  • The client already retries 8 attempts (jittered exponential backoff, Retry-After honor, billing fail-fast) and retries pre-first-delta stream failures — the real killer was the hardcoded 60s SSE idle watchdog: thinking models (GLM-5.3 etc.) routinely spend >60s before their first event, so healthy streams were killed and the retry budget burned against the same wall.
  • Idle watchdog default raised 60s → 120s and made configurable: new llm.stream_idle_timeout_seconds config section + ODEK_STREAM_IDLE_TIMEOUT_SECONDS env (env wins; floor 5s; applied in LoadConfig via llm.SetStreamIdleTimeout).
  • Post-delta idle aborts still never retry (would duplicate delivered text) — partial result surfaced with the error, unchanged.

Tests (RED-first)

New cmd/odek/subagent_tool_concurrency_test.go: shared limiter bounds peak across two instances (≤ cap), zero-capacity normalization (no deadlock), wait-event emission. New internal/config/llm_test.go (merge/clamp matrix) and internal/llm/set_idle_test.go (setter contract). All RED before the fix (undefined symbols + behavior), all GREEN after.

Verification

  • go test ./cmd/odek/ -run TestDelegateTasks -count=1 ✅ · new tests under -race
  • go test ./internal/config/ ./internal/llm/ -count=1 ✅ · go vet ✅ · go build ./... ✅ · gofmt clean ✅
  • go test ./cmd/odek/ -run TestReport ✅ (regression bar)

Docs

CONFIG.md (new llm section + env row), STREAMING.md (idle-watchdog section), SUBAGENTS.md (process-wide semantics), EXTENSIONS.md + AGENTS.md (subagent_concurrency_wait event type).

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 31, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
odek 3a5c8c0 Commit Preview URL

Branch Preview URL
Aug 31 2026, 11:39 AM

…able stream idle watchdog

delegate_tasks created its concurrency semaphore per tool call, so
sibling calls in one parallel batch (or concurrent serve sessions) could
exceed subagent.max_concurrency combined — saturating account-wide LLM
provider plans (observed: z.ai 429 storms with 8-subagent swarms).

- sharedChildSem: one process-wide limiter wired by builtinTools into
  every delegate_tasks instance; Call() waits via per-call WaitGroup
  (the old cap(sem) drain would permanently consume shared slots)
- maxConcurrency < 1 normalizes to 1 (unbuffered semaphore = deadlock)
- new subagent_concurrency_wait runtime event when tasks queue >5s
- runTaskFn test seam

internal/llm already retries 8 attempts incl. pre-first-delta stream
failures, but the SSE idle watchdog was hardcoded at 60s — thinking
models legitimately exceed that before their first event, so healthy
streams were killed and the retry budget burned (observed: 'llm: stream
idle for over 1m0s without an event').

- idle watchdog default 60s -> 120s, configurable via
  llm.stream_idle_timeout_seconds / ODEK_STREAM_IDLE_TIMEOUT_SECONDS
  (env wins, floor 5s, applied in LoadConfig via SetStreamIdleTimeout)
- post-delta idle aborts still never retry (would duplicate text)

New tests (RED-first): shared limiter bounds peak across instances,
zero-capacity normalization, wait-event emission, setter contract,
merge/clamp matrix. Sub-agent concurrency tests run under -race.
@jkyberneees
jkyberneees force-pushed the fix/subagent-concurrency-llm-retry branch from e260039 to 3a5c8c0 Compare August 31, 2026 11:38
@jkyberneees
jkyberneees merged commit ad1e13b into main Aug 31, 2026
10 checks passed
@jkyberneees
jkyberneees deleted the fix/subagent-concurrency-llm-retry branch August 31, 2026 11:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant