fix(subagents,llm): enforce process-wide child concurrency + configurable stream idle watchdog - #166
Merged
Merged
Conversation
Deploying with
|
| 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
force-pushed
the
fix/subagent-concurrency-llm-retry
branch
from
August 31, 2026 11:38
e260039 to
3a5c8c0
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 eventaborting turns).M1 — Process-wide sub-agent concurrency limiter
delegate_taskscall — sibling tool calls in one parallel batch (or concurrent serve sessions) each got their own cap, allowing up to N×subagent.max_concurrencychild streams against an account-wide provider plan.sharedChildSem,sync.Once) wired bybuiltinToolsinto everydelegate_tasksinstance — all calls and all serve sessions share a singlesubagent.max_concurrencybound. Entry points need no changes; children (depth-capped) keep their own process limit.Call()waits via per-callWaitGroup— the oldcap(sem)drain would permanently consume shared slots and deadlock later calls.maxConcurrency < 1normalizes to 1 (an unbuffered semaphore would deadlock the acquire-before-spawn loop).subagent_concurrency_waitruntime event when a task visibly queues on the limiter (threshold 5s), so the Web UI can show backpressure.runTaskFntest seam (nil = the realrunTask).M2 — LLM stream resilience
Retry-Afterhonor, 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.llm.stream_idle_timeout_secondsconfig section +ODEK_STREAM_IDLE_TIMEOUT_SECONDSenv (env wins; floor 5s; applied inLoadConfigviallm.SetStreamIdleTimeout).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. Newinternal/config/llm_test.go(merge/clamp matrix) andinternal/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 ./...✅ ·gofmtclean ✅go test ./cmd/odek/ -run TestReport✅ (regression bar)Docs
CONFIG.md (new
llmsection + env row), STREAMING.md (idle-watchdog section), SUBAGENTS.md (process-wide semantics), EXTENSIONS.md + AGENTS.md (subagent_concurrency_waitevent type).