Skip to content

mcpx: protocol follow-ups -- inline answers from scripts, cancellation, task status, call-scoped timeout #1864

Open

Description

@dezren39

Relocated from dezren39/darwin-nix#77 on 2026-10-28 (placement audit): mcpx's single source lives in developing-today/code (pkgs/mcpx); re-home tracked at #309. Original body below; issue references inside it are historical (dezren39/nix numbering).

Parent: #181

Summary

Four protocol gaps left after #61, each named in docs/protocol.md §6 and none tracked
elsewhere. Re-verified at 05c78b2: all four are still true, and §6 still lists them, which
is the right place for them to live — a gap written down in the document that describes the
surface is worth more than a gap in an issue. What this issue adds is the ordering and the one
real design decision among them.

Original notes

Not from the user's notes. Filed from the protocol work in #61:

Follow-ups from the protocol work (#61), each named in docs/protocol.md §6 but not tracked.

  • Questions raised inside mcpx_exec cannot be answered inline. Correlation identifies a call, and a script makes many upstream calls, so questions from a script go to the broker rather than back to the MCP client as input_required. A script-level correlation (the run id) would fix it.
  • Inbound notifications/cancelled does not interrupt the upstream call. A client cancelling a long tools/call stops waiting; the upstream server keeps working.
  • notifications/tasks/status from upstream is dropped. mcpx neither forwards nor records it.
  • pool.callTimeout is not call-scoped. The most obviously wanted per-call setting: --pool-call-timeout on one command should bound that command's call, but a daemon started with a different value ignores it. Needs threading through internal/pool (chore(deps): bump rustix from 0.37.19 to 0.37.25 in /sources/pavex/examples/realworld #56 left it, reasonably).

Current state (at 05c78b2)

docs/protocol.md §6 "What is still missing" (:368-382) lists all four, plus two the notes did
not mention: url-mode elicitation raised by mcpx itself, and OAuth for remote servers ("declared
and not performed"). The last one is a #177 instance — a declared capability that is not
delivered — and should be cross-linked there rather than quietly carried here.

Gap Verified at 05c78b2
Inbound cancellation does not interrupt the upstream call notifications/cancelled is sent by mcpx as a client (internal/mcpclient/client.go:715) and accepted by the test servers, but nothing routes an inbound one to an in-flight upstream call. internal/mcpclient/http.go:112 documents the decision: protocol-level cancellation was removed because pool.callTimeout was the bound that mattered. §6 states what is needed: "the request id plumbed through the pool".
notifications/tasks/status is dropped The string appears nowhere in internal/. §6: "mcpx polls instead."
A question from mcpx_exec cannot be answered inline §3.4's correlation identifies a call; §6 restates the consequence. Unchanged.
pool.callTimeout is not call-scoped internal/settings/registry.go:57 (Default: "120s"), read at internal/config/poolsettings.go:43-44 into the pool's configuration. There is no per-call override path.

Approaches

Three of the four have no real choice; one does.

pool.callTimeout per call — no real choice, and it is the one to do first. Thread an
optional per-call deadline from the request through internal/pool and let the pool's configured
value be the default. The only decision worth making is whether a caller may raise it or only
lower it, and that decision is not this issue's: it is #185 (ClampedBy — a daemon sets a
ceiling, a caller may lower but not raise). Take #185's answer. A daemon operator who sets 120s to
protect the machine should not be overridden by a script asking for an hour; a script that wants
30s should get 30s. Note the interaction the code already documents: internal/mcpclient/http.go
removed protocol-level cancellation because pool.callTimeout was the bound. Making the timeout
call-scoped therefore makes the cancellation gap below more visible, not less.

notifications/tasks/status — no real choice. Record it and expose it. mcpx already polls;
the notification is strictly better information arriving earlier. It lands in the event store
(#6), which is where anything with a seq belongs, and #6's gap semantics apply — a saturated
consumer must see a gap marker rather than silently miss a status change. Do it with or after #6.

Inbound cancellation — no real choice about whether, a real one about scope. A client that
cancels and an upstream server that keeps working is a resource leak with a user watching it. The
plumbing is what §6 says: carry the inbound request id into the pool so the in-flight call can be
found and cancelled. The decision is whether cancellation is best-effort (send
notifications/cancelled upstream, stop waiting, let the server finish in its own time — which is
all the MCP spec promises) or enforced (also tear down the pooled instance when the scope makes
that safe). Best-effort is correct by the spec and cheap; enforced is what a user means by
"cancel" when a runaway tool is eating the machine. Recommend best-effort by default with the
teardown available where the scope is caller-private — which is the same isolation question #190
and #191 are about.

The exec correlation gap — the one genuine design decision.

A — least recommended: give a script's questions a synthetic call id and answer them as if they
were a call.
Cheapest, and it looks right in a demo. It breaks as soon as a script makes two
concurrent upstream calls that both ask: the client is answering a question it cannot attribute,
and a wrong answer to an elicitation is worse than no answer. It also silently changes what §3.4's
correlation means, which is a documented wire contract.

B — keep questions from scripts on the broker, and make the broker reachable from the client.
Status quo plus a way for an MCP client to poll and answer broker questions in-band. No contract
changes, works today via /v1/elicit and mcpx_ask_*. The cost is that a modern client which
implements input_required correctly still gets a worse experience from mcpx_exec than from
mcpx_call, for no reason the client can see.

C — recommended: add a second correlation level — the run — and declare it in §3.4. A script's
questions correlate to the run id, which the exec surface already has and already exposes
(execsvc.Result.runId). A question raised inside a run is answerable inline by whoever invoked
the run, because there is exactly one of those, and the run id disambiguates concurrent upstream
calls without pretending they are one call. This is what the original note proposes and it is
right. It costs a documented change to §3.4's correlation rules and a revision-gated behaviour
(only clients that understand the run-level correlation get inline answers; the rest fall back to
B's broker), which is the pattern internal/mcpserver/revisions.go already implements for
FeatInputRequired.

Recommended because it is the only option that makes mcpx_exec a first-class citizen of the
question protocol rather than a permanent exception, and because the fallback to B is free.

Open questions

  • Does the run-level correlation need to nest — a script that runs a recipe that runs a script?
    §3.4's rules should state the nesting or forbid it explicitly.
  • Cancellation and tasks overlap: cancelling a call that was promoted to a task
    (/v1/tasks/{id}/cancel) already exists. Is inbound notifications/cancelled on a task-backed
    call the same path? If so, the plumbing is smaller than §6 suggests.
  • docs/protocol.md §6 also lists "OAuth for remote servers. Declared and not performed".
    That is a declared-but-undelivered capability and belongs in feat(nix): Android SDK + JDK 21 in the system and dev shells, and the OpenChamber release signing key #177's inventory. It is out of
    scope here; it should not stay only in a §6 bullet.
  • Is pool.callTimeout's interaction with mcpx exec's own timeout defined anywhere? Two
    timeouts with no stated precedence is how the next "flag accepted and ignored" (Update Rust crate petgraph to 0.6.4 - autoclosed #74) happens.

Ease · Impact · Related

Ease M overall — S for tasks/status, M for the call-scoped timeout, M for cancellation
plumbing, M–L for the run-level correlation and its revision gate.
Impact med-high — the call-scoped timeout is the most obviously wanted per-call setting in the
product, and a cancel that does not cancel is the kind of thing a user tells other people about.

Related: #181 (parent), #185 (ClampedBy — decides whether a caller may raise the call
timeout), #6 (event store — where tasks/status records belong, and whose gap semantics apply),
#112 (exec lifecycle hooks — shares the exec Frame ordering contract; #111 owns the rule for
adding a frame kind, collision 7), #177 (OAuth "declared and not performed" belongs in its
inventory), #78 (daemon state and caches), #56 (the settings pass that deliberately left the
per-call path alone), #61 (closed — the work these follow).

In flight: the PROTOCOL architect is working on these as this is written
(internal/mcpclient/**, internal/mcpserver/**, internal/pool/**, docs/protocol.md).
Re-check state before starting; parts of this may already be merged.

Security/auth deferred; see tracker.

Activity

  1. developing-today commented on Oct 28, 2026

    @developing-today
    Contributor
  2. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    History carried over from dezren39/darwin-nix#77 (verbatim, oldest first):


    @dezren39 · 2026-09-30T07:28:47Z

    Integration pass. This issue has no category tracker -- it predates the build-out and is a dependency of several parts of it. Placed in the proposed order at #181.


    @dezren39 · 2026-09-30T12:18:58Z

    Issues pass (2026-09-30, at 05c78b2). Restructured to the backlog template; the original
    text is quoted verbatim under Original notes and nothing was dropped.

    • All four gaps re-verified and still true. notifications/tasks/status appears nowhere in
      internal/. pool.callTimeout is internal/settings/registry.go:57 (default 120s), read at
      internal/config/poolsettings.go:43-44, with no per-call override path. Inbound
      notifications/cancelled is sent by mcpx as a client (internal/mcpclient/client.go:715) but
      nothing routes an inbound one to an in-flight upstream call — and internal/mcpclient/http.go:112
      records why: protocol-level cancellation was removed because pool.callTimeout was the bound
      that mattered. Making the timeout call-scoped therefore makes the cancellation gap more visible.
    • docs/protocol.md §6 carries two gaps this issue did not name, one of which belongs
      elsewhere: "OAuth for remote servers. Declared and not performed" is a declared-but-undelivered
      capability and belongs in feat(nix): Android SDK + JDK 21 in the system and dev shells, and the OpenChamber release signing key #177's inventory, not in a §6 bullet.
    • A/B/C added for the one item that has a real choice — the exec correlation gap. A synthetic
      call id breaks on two concurrent asking calls; the broker-only status quo makes mcpx_exec a
      permanent second-class citizen; a run-level correlation declared in §3.4, revision-gated the
      way FeatInputRequired already is
      , is recommended. The other three are marked "no real
      choice" with the one decision each carries.
    • The call-timeout decision is deferred to Phase 5: who may clear a tag claim #185 (ClampedBy): whether a caller may lower the
      ceiling only, or raise it. That is Phase 3: sync, blob gate, and relays #181 collision 5 and it is not this issue's to settle.
    • Parent: #181 and an Ease/Impact/Related footer added.
    • In flight: PROTOCOL is working on these now (internal/mcpclient/**, internal/mcpserver/**,
      internal/pool/**, docs/protocol.md). Re-check before starting.

    @dezren39 · 2026-09-30T12:26:18Z

    Issues pass (2026-09-30, at 05c78b2) -- for whoever lands this. This issue is in flight, so its body was not edited. These came out of re-checking every other issue against current main; each is verified at 05c78b2 unless marked.


    @dezren39 · 2026-09-30T14:21:56Z

    First bullet (questions raised inside mcpx_exec answered inline by the MCP client) implemented in #236: correlation by run id, one attribution rule shared with single calls (every in-flight call on the key must belong to the run, counted by the pool), both eras. Remaining bullets: inbound cancellation now reaches the upstream call on stdio and HTTP (#198); upstream notifications/tasks/status and call-scoped pool.callTimeout remain open here.


    @dezren39 · 2026-10-01T21:58:15Z

    Inbound notifications/cancelled from an upstream server is now handled by mcpx as a client (branch fix4/client-gaps, 316f1e7, combined fix4 PR): requests the server sent mcpx (elicitation, sampling, roots) are tracked by id; a cancellation naming one ends the handler's context (so a question relayed to the host is withdrawn) and no response is sent. The reason goes to OnWarning → server.warning in the lifecycle log. Gap records removed for client cancellation-receiver-should-stop-free-no-response, cancellation-processing-should-cease and cancellation-log-reasons; those 13 cells now run and pass, and fail with cancelAsked removed ("answered=true stopped=false").

    Still open here: server-side cancellation-log-reasons (reason reaches Server.OnCancel, which nothing outside tests sets), and the other rows in this issue's body (tasks/status, exec questions, per-call timeout).


    @dezren39 · 2026-10-28T20:45:58Z

    Triage verdict (lane A, task 2049, 2026-10-28): CONFIRMED at current HEAD, with one sub-item partially advanced — routed code-side; kept open under #181.

    Evidence (owning repo developing-today/code @ bfcfc15; the mcpx source left dezren39/nix on 2026-10-19, #468):

    • pkgs/mcpx/docs/protocol.md §6 "What is still missing" still lists these gaps at HEAD — the document that describes the surface still names all of them.
    • notifications/tasks/status: still dropped. The string appears only in the vendored spec schema (internal/mcpspec/schema/2025-11-25/schema.json) and conformance data (internal/conformance/requirements.json) — no handler anywhere in internal/.
    • pool.callTimeout: still not call-scoped. It is read once from settings into the pool configuration (internal/config/poolsettings.go:55-56, registry default at internal/settings/registry.go:107); there is no per-call override path.
    • Partial progress since filing: §6 now says direct-call cancellation works (notifications/cancelled / closed stream cancel the upstream request); the remaining cancellation gap is the ask path, where a call runs as a daemon task and keeps running. The inline-answer gap persists in its current form (-32021 deliberately not raised; broker-fallback question is ⚡ Bolt: Optimize search match counting using pre-computed highlights #252 item 1).

    Routing: no follow-up packet from this triage — this is protocol design work in developing-today/code (re-home tracked by #309, OPEN), and its own sequencing stands: the callTimeout item takes #185's ClampedBy answer, and the tasks/status item lands with or after #6's event store. No counterpart for #77 exists in code yet, so #77 stays open under tracker #181.

  3. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 29, 2026
  4. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  5. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · triage verdict — RESCOPED: one item already landed, one partially landed, one deferred to #1554, one follow-up enqueued

    Verdict: the issue's snapshot is stale in the way its own "In flight" warning predicted — the protocol work has since landed parts of this. Per-item dispositions below; only the call-scoped timeout is enqueued. No duplicate issue exists for any item (dedupe searches return #1864 alone, plus #1554 for the tasks overlap noted).

    Verified fresh against current main 82b019a0, chiefly pkgs/mcpx/docs/protocol.md as rebuilt (§3.4, §3.5, §6 "What is still missing", dated 2026-09-30) plus code reads:

    1. Inline answers from scripts (run-level correlation) — ALREADY LANDED, as the issue's recommended Approach C. §3.4 as built: a script (mcpx_exec) runs as an interruptible call (/v1/ask kind exec); its run id is chosen and registered before the script starts; the generated client sends it as X-Mcpx-Run on every /v1/call, so upstream calls join the ask table as members of the run; a question on a key whose in-flight calls all belong to one run is that run's — parallel calls included; ambiguity (two runs, or a run plus anyone else) goes to the broker, and run membership is removed when the run ends. That is Approach C's design, including the broker fallback. Current §6 no longer lists this gap. No packet; nothing left of this item except this note. (The nesting question the issue raised is answered implicitly by the as-built rule — membership is per run id, and a nested run is a different run sharing a key means ambiguity → broker. If that is not the intended semantics, it is a §3.4 documentation point, not an implementation gap.)
    2. Inbound cancellation — PARTIALLY LANDED; remainder rescoped and stays on this issue. Current §6: "On a direct call, notifications/cancelled (stdio, legacy HTTP session) and a closed stream (2026-07-28 HTTP, sessionless legacy) cancel the upstream request." What remains is exactly the issue's open question: "A call on the ask path runs as a daemon task and keeps running (§3.5)." Since ask-path calls ARE daemon tasks and /v1/tasks/{id}/cancel already exists, the remainder is plausibly the smaller plumbing the issue hoped for — but that equivalence must be confirmed by whoever implements it, so no packet is enqueued from triage: the item stays open here, rescoped to ask-path/task-backed calls only, with the direct-call part recorded as done.
    3. notifications/tasks/status dropped — STILL OPEN, deferred to design(mcpx): tasks, finished — one task store for MCP and /v1, and a client that can call task-required tools #1554 by design, not neglect. Current §6 still lists it ("notifications/tasks/status is dropped; mcpx never task-augments a call upstream…"). The issue's own approach says record-and-expose lands in the event/task store — and the one-task-store design now exists as design(mcpx): tasks, finished — one task store for MCP and /v1, and a client that can call task-required tools #1554 ("tasks, finished — one task store for MCP and /v1…", open). Recording task status into a store that is being redesigned would be throwaway work; this item should land with or after design(mcpx): tasks, finished — one task store for MCP and /v1, and a client that can call task-required tools #1554. Cross-linked there by this verdict's reference; it stays listed on mcpx: protocol follow-ups -- inline answers from scripts, cancellation, task status, call-scoped timeout #1864 so it is not lost if design(mcpx): tasks, finished — one task store for MCP and /v1, and a client that can call task-required tools #1554's scope shifts.
    4. pool.callTimeout not call-scoped — STILL OPEN; follow-up enqueued (the issue's "do first" item). internal/config/poolsettings.go ~L55-56 still reads pool.callTimeout only from settings into the pool's (per-server) configuration; internal/config/config.go's CallTimeout is a per-server config field throughout (merge at ~L855) — no per-request/per-call override path exists, so --pool-call-timeout on one command still cannot bound that command's call. (One facet did land since the snapshot: §3.5 — pool.callTimeout now pauses while a question is pending on the instance. That is not call-scoping.) The follow-up carries the issue's settled rules: thread an optional per-call deadline through internal/pool; the daemon's configured value is the ceiling — a caller may lower it, never raise it (the ClampedBy rule this issue adopts); and the precedence between the call-scoped timeout and mcpx exec's own timeout must be written down in the same change (the issue's open question — two timeouts with no stated precedence is how the next "flag accepted and ignored" happens).

    Dedupe: per-item searches (call-scoped timeout; notifications/cancelled upstream; tasks/status; run correlation) return #1864 as the only scoped issue, with #1554 the owning design for item 3's store. Nothing closed as a duplicate.

    This issue stays open as the home for items 2 (remainder) and 3; items 1 is done and 4 is queued.

  6. dezren39 commented on Oct 29, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · triage complete

  7. 136 remaining items

  8. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    agent-note:v1
    state: parked
    queue: repo-health-code-pilot
    task: 9002 (small_fix, worker repo-worker-code, lane aea12c82-c5af-4e5a-9b66-f8e360f87177)
    by: openchamber-scheduled-lane
    at: 2026-11-04T07:14Z

    Implementation and targeted tests are complete on branch queue/9002-1864-ask-cancel in /home/user/git/fleet/9002; go test ./internal/mcpserver -count=1 and go test ./internal/cli -count=1 pass. Parked at the run budget before commit/PR. Resume: assess the recorded transport-boundary cancellation race, run git diff --check, commit/push, open the PR, and verify CI. Queue resume is scheduled in 20 minutes.

  9. added
    queue/claimedQueue worker lifecycle state
    and removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Nov 4, 2026
  10. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    agent-note:v1
    event: handed_off
    task: 9002
    lane: code-m (worker repo-worker-code)
    execution_lane: opencode
    note: Re-claimed by lane code-m at 2026-11-04T07:35Z; execution-lane assignment is opencode, so the task is handed off — the laptop opencode claimant launches the bound session. Resume state preserved in task 9002 checkpoints: branch queue/9002-1864-ask-cancel at laptop /home/user/git/fleet/9002, stage targeted_tests_passed, no commit/PR yet; remaining: assess the recorded transport-boundary cancellation race, commit/push, open PR, verify CI.

  11. dezren39 commented on Nov 5, 2026

    @dezren39
    MemberAuthor

    agent-note:v1
    event: handed_off
    task: 9002
    worker: repo-worker-code (lane code-f)
    stage: handed_off
    summary: Task 9002 re-claimed by lane code-f (attempts 16/16); execution-lane assignment again returned assigned_lane=opencode with session_id null, so this lane hands off per the runbook. Resume state for the claimant: laptop worktree ~/git/fleet/9002 on branch queue/9002-1864-ask-cancel — implementation and targeted suites complete per prior checkpoints; remaining: assess cancellation race, commit/push, open PR. Flagging the attempts cap: if this claim's lease expires unlaunched, the task hits max_attempts and will need curator/monitor attention.

  12. dezren39 commented on Nov 5, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  13. dezren39 commented on Nov 5, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

  14. added
    queue/queuedHas a queued task in the Supabase agent queue
    and removed
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    on Nov 5, 2026
  15. dezren39 commented on Nov 5, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · handed off

    • Agent: fleet monitor
    • Session / ID: task 9002 (repo-fleet-monitor run 2026-11-05T15:39:47Z)
    • Found via: monitor reconciliation (Duty 1 claim/label divergence repair)
    • Task: Claim evaporated: task 9002 is queued and unclaimed in repo-health-code-pilot, but this issue still carried claimed labels. Labels reset to the truth (queue/queued + status/needs-triage). Checkpoint stage at evaporation: handed_off.
    • Expectation: The next lane claim picks task 9002 up from the queued state and posts a fresh pickup note.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or requestqueue/queuedHas a queued task in the Supabase agent queuesource/creatorFiled by the new-issue-creator jobstatus/needs-triageNot yet triaged; needs a first looktask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions