Skip to content

5.7.0: one authorization URL per connect, not one per call - #36

Merged
dickhardt merged 1 commit into
mainfrom
fix/one-url-per-connect
Oct 2, 2026
Merged

dickhardt merged 1 commit into
mainfrom
fix/one-url-per-connect

Conversation

@dickhardt

Copy link
Copy Markdown
Contributor

Why

Prod, 2026-09-28 16:16–16:18 UTC. Claude Code 2.1.283 on 2026-07-28 called connect_resources once with four Google items. Gmail and Calendar both answered requirement=interaction. The person got two URL elicitations:

The Person Server queues every pending interaction per person and drains the queue into the open tab. The second URL was not needed.

What changes

  • One URL per connect. Pass 1 starts both live items, then the call hands over one URL, the head's. Every other live code at the same interaction endpoint is marked coveredBy that URL, and so is a code started while that URL is out. A retry waits on covered codes through pass 2 with progress notifications.
  • When a further URL is handed over. Only if (a) a poll re-advertises the code (requirement=interaction), or (b) the code has sat at the head of the queue (queue_position 1) for 30 s with the poll still answering status: 'pending', not 'interacting'. Why 30 s: the tab gets every queued code when it connects and acks within about 1 s (16:17:19.8 → 16:17:20 in the incident). But a poll held by Prefer: wait=20 answers with the record as it was when the poll arrived, so it can be 20 s stale. 30 s after first seeing the code at the head leaves at least 10 s. Covered codes are polled in 15 s slices so the check isn't whole rounds late. A PS that sends no status counts as pending, so its covered codes fall back to their own URL instead of waiting out CONNECT_MAX_MS.
  • onInteraction is a hand-over hook. It is called once per URL handed over and is expected to return. A throw still passes through, with the URL recorded as handed over first. invoke now elicits natively after the hook returns, as connect_resources does.
  • pollConnection returns the 202 body's status, queuePosition and queueDepth, plus advertised when that poll carried requirement=interaction. A new stopOnAdvertise option ends a slice on a code the caller has not handed over. The poller is the proxy's own pollUntilDone; @aauth/mcp-agent is not used.
  • Fix: a re-advertisement without Location is now recognised. Wallet poll.js re-advertises with AAuth-Requirement and Retry-After only, and interactionFrom required Location, so poll re-advertisements were dropped.
  • Fix: surfaceNatively reads client capabilities from the 2026-07-28 request envelope. serveStdio does not backfill getClientCapabilities(), so the stdio bin fell back to text on that revision.
  • tool.call records outcome: 'input_required'.

Tests

connect-resources.test.ts, with real SDK clients on both eras:

  • 2026-07-28: four items, first two interaction. One input_required carrying one URL. The SDK driver's retry lands all four. onInteraction is called once.
  • 2025-era: one -32042 with one URL. The retry lands all four.
  • A covered code still pending at the head past the bound gets its own URL, not before the bound.
  • A covered code that is interacting gets no URL however long it waits.
  • A re-advertised covered code gets its URL at once, and only once.
  • A code started while a URL is out is covered.

Also: connect.test.ts covers the poll fields and the Location-less re-advertisement. invoke-resume.test.ts covers invoke eliciting natively once.

npm run typecheck, npm test (220 passed), npm run build.

Known gap (Wallet, not changed here)

If the person closes the wallet tab after it acked the queued codes, those codes stay interacting. The PS's stranded-delivery guard (poll.js) re-advertises only records created with reach_delivery: true. A covered code in that state waits until CONNECT_MAX_MS (10 min) and answers timed_out.

Not merged or published. That waits for Dick.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CEukeoQvjrsXzfmBNDoZN3

Prod, 2026-09-28: a four-item connect_resources with Gmail and Calendar both
answering requirement=interaction handed the person two URLs. onInteraction
threw on the first item, so Calendar was never started; the retry started it
and elicited again, for a code the person's wallet tab already held.

- connect_resources starts every live item before handing over a URL, then
  hands over one — the head's — and marks the other live codes at the same
  interaction endpoint `coveredBy` it. A code started while a URL is out is
  covered too. A retry waits on covered codes through pass 2 with progress.
- A covered code gets its own URL only when a poll re-advertises it, or when
  it has been at the head of the queue (queue_position 1) for 30 s with no
  browser holding it (poll status `pending`, not `interacting`). A poll held
  by Prefer: wait=20 can answer up to 20 s stale, so 30 s after first seeing
  the head leaves at least 10 s for an open tab to take it. Covered codes are
  polled in drainMs/2 slices so the check is not rounds late.
- onInteraction is called once per URL handed over, not per code minted, and
  is expected to return. A throw is still passed through. invoke now elicits
  natively after onInteraction returns, as connect_resources does.
- pollConnection returns the 202 body's `status`, `queuePosition`,
  `queueDepth`, and `advertised` when that poll carried
  requirement=interaction. `stopOnAdvertise` lets the caller end a slice on
  a re-advertised code it has not handed over.
- A re-advertised code without a Location header is recognised: Wallet
  poll.js re-advertises with AAuth-Requirement only, which was dropped.
- surfaceNatively reads capabilities from the 2026-07-28 request envelope;
  serveStdio does not backfill getClientCapabilities(), so the stdio bin fell
  back to text on that revision.
- tool.call records outcome: input_required.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CEukeoQvjrsXzfmBNDoZN3
@dickhardt
dickhardt merged commit a59ea48 into main Oct 2, 2026
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