Skip to content

Design: mcpx client session & stream recovery — re-initialize on a legacy session 404, re-issue a broken modern request once #1634

Closed
3 of 3 issues completed

Description

@dezren39

Summary

mcpx's client has no recovery from the two ways a live upstream connection goes stale. When a legacy HTTP upstream restarts and forgets the session, every later call 404s forever — the client never re-initializes, and the pool cannot reap an HTTP client that never dies, so only a daemon restart heals it. When a modern (2026-07-28) upstream's response stream breaks, the in-flight request is reported lost and never re-issued, though the 2026 spec makes re-issue a client MUST. This design puts both recoveries in Client (the layer that owns the handshake, the pending table and the era): forget + re-initialize + retry once on a session-carrying legacy 404, and re-issue once with a new id on a broken modern stream — with tools/call re-issued only on annotation evidence, so a destructive tool is never silently executed twice.

Design doc: files/designs/code-mcpx-client-session-stream-recovery.md in the keep-the-four-repos-healthy goal workspace (full audit, alternatives, risks, open questions).

No Drew direction decision needed: the roadmap row is at proposed, both behaviours are spec MUSTs (TR-29; 2026 changelog / PM-06), and the judgment calls (client-layer recovery; the annotation gate) are recorded in the design doc.

Current state (verified on main ed8e591, 2026-10-28)

  • The two gaps are open in the conformance register, pkgs/mcpx/internal/conformance/coverage_transport.go:114-115: streamable-http-client-reissue-broken-request and streamable-http-client-reinit-on-404, both client-side, origin mcpx client: legacy Streamable HTTP and stdio robustness — session 404, GET stream, SSE retry, string ids, stream timeout, version header dezren39/darwin-nix#203.
  • 404 path: HTTPTransport.Send (internal/mcpclient/http.go ~L166-180) returns any non-2xx as HTTPStatusError (except a JSON-RPC error body naming the request, pushed as the reply). Nothing in Client treats a session-carrying 404 as expiry. Register TR-29 (docs/compare/register/transports.md:550-565): a restarted remote server "leaves its mcpx instance failing every call"; legacy spec (2025-03-26..2025-11-25): a client receiving 404 for a session-carrying request MUST start a new session with a fresh initialize.
  • Broken-stream path: a modern SSE response stream that ends unanswered calls t.lost(reqID) (http.go ~L190-205), synthesising a -32000 error (lost/codeStreamLost ~L378-389); the caller just fails. Legacy streams instead resume via GET + Last-Event-ID (resume(), http.go ~L338) — legacy has resumability, 2026 deliberately does not (PM-06, docs/compare/register/process-model.md:133-146).
  • Reusable machinery: Client.roundTrip (internal/mcpclient/client.go ~L953) is the single request choke point; initializeLegacy/finishLegacy (~L343-388) already validate the version, refresh ServerInfo/Capabilities and restart the GET listen stream; the catalogue-invalidation signal (invalidate → Notifications.OnListChanged, ~L683-749) already reaches the pool; Tool.Annotations (~L159-170) carries readOnlyHint/idempotentHint/destructiveHint.
  • Distinct from design(mcpx): resume upstream HTTP sessions after a cold restart — persist the resumable session state #1414: that design resumes a still-live session after our daemon restarts. This one handles the opposite event — the upstream restarted and the session is dead; the move is discard + new session, not resume.

Proposal (decisions recorded in the design doc)

  • Recovery lives in the client. Not the transport (protocol-free; initialize is a client exchange) and not the pool (reaping loses the in-flight call, and nothing marks an HTTP client dead for the pool to reap).
  • Legacy 404 (Phase 1): trigger only when a request (not a notification, not initialize) 404s, era is legacy, and the request carried a session id. The transport gains a report-and-clear session seam (the setNegotiated interface-assertion pattern); re-initialize is singleflight across concurrent calls (session generation — one initialize per upstream restart); the request retries once with a new id; a second 404 surfaces. After re-initialize, all three catalogue kinds are invalidated through the existing signal.
  • Modern broken stream (Phase 2): the stream-lost error gets a stable identity (not message matching); the request layer re-issues once with a new id. tools/call is gated: automatic re-issue only when the tool's list-time annotations declare readOnlyHint or idempotentHint (PM-06's double-execution hazard is the register's own warning); otherwise the stream-lost error surfaces and the host decides. Legacy is excluded — resumability is its mechanism.
  • Truth-up (Phase 3): both conformance gaps retired with test evidence; TR-29 and PM-06 register rows rewritten as-built; the roadmap's "Legacy client robustness" row updated to name the umbrella's remainder; evidence comment on mcpx client: legacy Streamable HTTP and stdio robustness — session 404, GET stream, SSE retry, string ids, stream timeout, version header dezren39/darwin-nix#203 (comment, not close — the umbrella is its owners').

Phases (each independently landable; filed as sub-issues)

  1. Legacy session-404 re-initialize + retry once — transport seam, singleflight client recovery, one retry, catalogue invalidation; gap streamable-http-client-reinit-on-404 retired.
  2. Modern broken-stream re-issue, annotation-gated — stable stream-lost identity, once-only re-issue with a new id, annotations retained at list time, tools/call gate; gap streamable-http-client-reissue-broken-request retired. Independent of Phase 1.
  3. Register truth-up + nix#203 evidence — TR-29 / PM-06 rows as-built, roadmap row updated, evidence comment on mcpx client: legacy Streamable HTTP and stdio robustness — session 404, GET stream, SSE retry, string ids, stream timeout, version header dezren39/darwin-nix#203. Gated on Phases 1–2.

Acceptance (design level)

Acceptance criteria

Activity

  1. added
    priority/normalOrdinary priority (the default)
    app/mcpxPackage/app: mcpx (pkgs/ or apps/ in this repo)
    epicMeta-issue grouping multiple items (org Project Type=Epic)
    on Oct 28, 2026
  2. developing-today commented on Oct 28, 2026

    @developing-today
    Contributor
  3. added
    kind/designA design proposal (design-architect): multi-step, phased, decision-bearing
    on Oct 28, 2026
  4. added
    effort/lLarge: multi-day or open-ended; expect staged check-ins
    meta/enrichedIntake review done: acceptance criteria, links, and normalized labels added
    on Oct 28, 2026
  5. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    Intake review

    Related: Design epic for mcpx client session/stream recovery; its three phase issues #1637 (legacy 404 re-init), #1638 (modern re-issue) and #1639 (register truth-up) are already filed and in the queue — this issue tracks the set, it should not be worked as a standalone fix.

    Notes / risks: The destructive-tool double-execution risk is the design's central hazard; the annotation gate on tools/call re-issue must survive implementation unchanged.

    Suggested approach: Confirm closure against the phase issues and the conformance register, not by a single PR.

    Live check: verified still open and unheld this run; a merged-PR search on this run (2026-10-28T10:35:00Z) returned no PR resolving this issue, so it remains live work. Acceptance criteria appended to the body (original text preserved verbatim above it) and labels normalized by the fleet monitor.

  6. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    Program tending — all sub-issues closed, proposing closure

    Program-tender check (2026-10-29): all 3 sub-issues of this design epic are now closed — #1637 (closed 2026-10-28T16:31Z), #1638 (2026-10-28T15:27Z), #1639 (2026-10-29T01:13Z). The mcpx roadmap's Legacy client robustness row already records #1637/#1638 as landed.

    Proposing this epic be closed as complete. If a follow-on phase is still intended (rather than filed later as its own design), say so here and keep it open — otherwise closure keeps the epic list honest.

  7. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed on Nov 1, 2026
  8. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    Picked up for triage under repo task 2618. I’m verifying the linked phase issues and current main before recording the verdict.\n\n

  9. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    Triage verdict — complete; already implemented

    Issue #1634 is complete. Its recovery requirements have landed, so no follow-up packet is needed:

    Verified on fetched current main a8189c2: legacy session-404 recovery is in pkgs/mcpx/internal/mcpclient/client.go:1215 and http.go:702, with TestLegacySession404Recovery in reinit_test.go:96; modern broken-stream re-issue is in client.go:1140 and http.go:411, with TestModernReissueBrokenRequest in reissue_test.go:107. The conformance rows and TR-29/PM-06 register and roadmap are also updated.

    Focused verification: ok github.com/developing-today/code/pkgs/mcpx/internal/mcpclient 0.081s — PASS. The broader had mcpclient PASS and unrelated conformance failures in TestAuthSplit/TestMatrix (authsplit fixture), TestMessagesClient/messages-resulttype-extension-values, and TestUtilitiesServer/completion-max-100 on this clean main snapshot.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/mcpxPackage/app: mcpx (pkgs/ or apps/ in this repo)effort/lLarge: multi-day or open-ended; expect staged check-insepicMeta-issue grouping multiple items (org Project Type=Epic)kind/designA design proposal (design-architect): multi-step, phased, decision-bearingmeta/enrichedIntake review done: acceptance criteria, links, and normalized labels addedpriority/normalOrdinary priority (the default)source/design-architectFiled by the repo design architect job

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions