sirens-deep-forgejo-mcp: request hangs exactly 180s, then connection closes #49
Labels
No labels
autonomy
async-consult
autonomy
epic
autonomy
headless
autonomy
live-collab
coherence-core
priority
P0
priority
P1
priority
P2
priority
P3
priority
P4
qa-fixture
role/ai
role/creator
role/design
role/director
role/engineer
role/exec
role/human
role/ops
role/qa
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
coilyco-flight-deck/mcp-beaver#49
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Symptom
A single MCP request to
sirens-deep-forgejo-mcphung for 180 seconds and then failed on a closed connection. The calling turn had already abandoned it.Evidence
Trace
883ba8968cd370c32d678168dbad68b5, 2026-08-12T07:57:41Z (SigNoz:http://ser8:30808/trace/<trace_id>):HTTP POSThttp://sirens-deep-forgejo-mcp:8080/mcp180002440885ns = 180.002s200Errorread tcp 10.42.0.147:35628->10.43.160.192:8080: use of closed network connectionTwo things stand out:
10.42.0.147is thesirens-deeppod;10.43.160.192:8080is the forgejo MCP service.Impact
This is the main contributor to
sirens-deep's p99 of 120.5s over 36 calls (11.1% error rate). It also produces user-visible silence in Discord, separately from theungrounded_action_claimfailures tracked incoilyco-gaming/sirens-echo.Pod state rules out a crash
At the time of failure,
sirens-deep-forgejo-mcp-78d5f7fcbf-xwlqwwasRunningwithrestart_count: 0, up 11h37m (ward-mcp:97a100274a72a281b11440af89a9f7638c2754cb). Thesirens-deeppod was alsoRunning,restart_count: 0, up since 07:35:59Z. Neither process died — the request hung inside healthy pods.Open questions
Acceptance
Next owner
Engineer.
server-infoshould be on by default — the disclosure argument for opt-in does not survive reading the payload #61Fixed in
719795b. Answering the three open questions from the runtime rather than from the trace, because reading the source turned up more than the span did.Question 1 - who owned the 180s. Neither side owned it deliberately. Three things were unbounded at once:
http.Serverwas constructed with no timeouts at all - no read-header, no idle, nothing.http.DefaultClient, which has no timeout. Spec mode was already bounded by opcore's 30s default client, which is why this shape is easy to miss.StreamableHTTPHandlerdoes not tie handler work to the originating HTTP request unlessPropagateRequestCancellationis set, and it was not.That third one is the direct explanation for the orphaned root span. The caller had gone away and the work kept running, because nothing connected the two. It was not a stray span - it was an accurate picture of a request that genuinely outlived its caller.
Question 3 - yes, and it is now
--request-timeout, default 60s. Applied at the tool handler rather than only at the edge, deliberately: the SDK propagates cancellation only for >= 2026-07-28 clients, so a bound written purely at the transport would look correct and do nothing for an older client - which is the case that hung.Question 2 - the method is now on the transport span.
mcp.method.name, andgen_ai.tool.namewhere there is one, are stamped on the enclosing span before dispatch, so a request that never returns still names what was in flight. Both values are already bounded to closed sets, so this adds no cardinality a caller can drive.Two things that cost a probe to find, both now pinned by tests, because both would have shipped as false green:
/healthzis exempt from every deadline: a liveness probe a wedged upstream can fail turns one slow dependency into a restart loop.Acceptance:
PropagateRequestCancellation. Older clients cannot be told the caller left, so for them the per-call deadline is the bound, and the span ends at 60s rather than never.Worth noting for the deploy side: 60s is a runtime default, not a Discord-appropriate one.
sirens-deepshould probably set--request-timeoutwell below that.