Skip to content

Ask the user native questions and approvals on the Rust backend - #432

Merged
ifdotpy merged 1 commit into
mainfrom
fix/native-questions-rust
Oct 22, 2026
Merged

ifdotpy merged 1 commit into
mainfrom
fix/native-questions-rust

Conversation

@ifdotpy

@ifdotpy ifdotpy commented Oct 22, 2026

Copy link
Copy Markdown
Owner

Symptom

Live build 0ec04e4, 2026-10-22: the Claude lead c8b8b265 (a single-agent chat on the host) called AskUserQuestion with two questions. The chat showed the options and then "Error: Tool permission request failed: Error: Provider callback is not supported by this runtime". The agent continued without the user's answer.

Cause

account_worker::start_callback sent only item/tool/call and the VM token refresh to handlers. Every other provider request went to callback::response, whose dispatch always returned Unsupported (-32601). The card code in provider_callbacks::cards and the /api/answer runtime already existed, but the worker never admitted a card.

Fix

  • The account worker admits each native approval or question of its account session as a pending request card (provider_callbacks::admit_native). The card owns the answer, and the worker retires the request's callback record. A replay after a restart finds the same card and sends nothing.
  • A worker's question still gets the exact refusal "Only the orchestrator can ask the user. ..." that the bridge maps to a deny. A deleted agent still gets the decline answers.
  • The user's answer goes out once through the existing /api/answer runtime. BrokerCallbackReplyPort now uses RetainedSessionPort::deliver_callback_reply, which matches the session by account and provider generation. A replacement worker of that generation has a new connection id, and the provider process still waits (the cause of End the turn and its commands when Studio stops or deletes an agent #423 too).
  • serverRequest/resolved finds the cards that the Rust worker stores. It matched only the connection-<generation> name that Python used.
  • The callback context accepts a turn that waits in approval, so the other tool calls of that turn still work. A request without a turn id (an MCP elicitation) uses the thread's active turn.
  • A card of an ended provider generation expires in the worker's startup recovery. Before, it would wait forever and could not be answered or deleted.
  • Stop and delete already expire pending requests (agent_management::stop) and change the epoch, so a late answer is refused. The bridge drops its own wait on interrupt.

Provider callbacks that the Rust worker answered Unsupported and Python supported

Request Python Rust before This PR
item/tool/requestUserInput (Claude AskUserQuestion, Codex) card -32601 card
item/commandExecution/requestApproval (Claude tool approvals, Codex) card -32601 card
item/fileChange/requestApproval card -32601 card
item/permissions/requestApproval card -32601 card
mcpServer/elicitation/request card -32601 card
currentTime/read {currentTimeAt} -32601 not changed: the client renders no card for it
the five native requests from a Linux VM actor card -32601 not changed: the card answer path writes only to the account session

account/chatgptAuthTokens/refresh is not a gap: Python answered it only for VM connections, and Rust has its own VM path.

Tests

  • New account_worker::tests::native_requests (fake provider, real account worker, CallbackRuntime::answer as /api/answer calls it). All five original cases failed before the fix with the live answer: {"error":{"code":-32601,"data":{"kind":"unsupported_callback"},"message":"Provider callback is not supported by this runtime"}}.
    • a lead question waits as a card, the agent is in approval, the answer reaches the provider once, and a repeated answer replays without a write
    • a worker question gets the orchestrator refusal and stores no card
    • an approval carries the user's decision; serverRequest/resolved closes a second one, and it takes no answer
    • an MCP elicitation without a turn id is answered with its form content
    • a pending question survives a worker reattach with a new connection id and is answered once
    • a new provider generation expires the old question
  • The new module joins the short tier selection (facade.mjs).
  • Tests that used an approval without a thread as a stand-in for an unsupported callback now use an unknown method (callback_handoff, the callback burst), or assert the new refusal (crash matrix scenarios 9 and 10, renamed in mutations.json).
  • cargo nextest run -p studio-runtime-services (2000 tests) passed except two that the environment caused and that pass alone: startup_recovery_corpus needs CODEX_NODE because its oracle runs with PATH=/usr/local/bin:/usr/bin:/bin, and a browser_host_flow port race.
  • cargo nextest run -p studio-runtime-server --bins: 107 passed. runtime-http callback, question and answer tests: 5 passed.
  • Clippy -D warnings: the runtime-services lib and lib tests are clean. The only findings are unused imports in two files this PR does not touch: runtime-services/tests/provider_session_task.rs:717 and runtime-server/tests/integration.rs:8.
  • Manual browser check (not committed, because new tests must fit the short tier): the Rust UI fixture's scripted provider sent item/tool/requestUserInput; the web card showed the question; Answer, "APFS clone", Send answer; the provider's turn continued, and /api/questions returned the card as answered with that answer.
  • questions-ux-ui.spec.mjs fails at line 76: it waits for a card with id async-question, which only the Python fixtures seed; the Rust UI fixture has no such seed. question-history-ui.spec.mjs passes.
  • just test-fast on 6b723c8 (rebased on bae1e04): passed, 545 s.

Not covered

  • A live model request.
  • VM actor native requests and currentTime/read (table above).
  • The partial index runtime_agent_callback_context_v2 (status running only) is now unused; the lookup uses runtime_agent_thread. I left the schema unchanged during the freeze.
  • If serverRequest/resolved arrives before the worker admits the card, the card stays pending. The bridge resolves only after its approval timeout, so this needs a resolution within milliseconds of the request.

A Claude lead (c8b8b265, 2026-10-22) called AskUserQuestion. The bridge
sent item/tool/requestUserInput, the Rust account worker answered -32601
"Provider callback is not supported by this runtime", and the agent went
on without the user's answer. The worker sent only item/tool/call and the
VM token refresh to handlers. Every other provider request got the
unsupported answer, although the card code and the /api/answer runtime
existed: the worker never admitted a card.

- The account worker admits each native approval or question of its
  account session as a pending request card: command, file and permission
  approvals, item/tool/requestUserInput and MCP elicitation. The card owns
  the answer, and the worker retires the request's callback record. A
  worker's question still gets the orchestrator refusal. A replay after a
  restart finds the same card and sends nothing.
- The user's answer goes out once through the existing /api/answer
  runtime. The registry reply port matches the session by account and
  provider generation. A replacement worker of that generation has a new
  connection id, and the bridge still waits (the cause of #423 too).
- serverRequest/resolved finds the cards that the Rust worker stores. It
  matched only the connection name that Python used.
- The callback context accepts a turn that waits in "approval", so other
  tool calls of that turn work. A request without a turn id uses the
  thread's active turn.
- A card of an ended provider generation expires at worker start.
- Requests that a Linux VM actor relays keep the unsupported answer: the
  card answer path writes only to the account session.

Tests: account_worker::tests::native_requests (fake provider; failed
before the fix with the live -32601 answer) joins the short tier. Tests
that used an approval without a thread as a stand-in for an unsupported
callback now use an unknown method, or assert the new refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ifdotpy
ifdotpy merged commit 99c3979 into main Oct 22, 2026
8 checks passed
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