Auto-accept codex elicitation requests + pluggable inbound-request handlers (fix wedge — internal#659 P1#1 part 2) #49
Reference in New Issue
Block a user
Delete Branch "fix/auto-accept-elicitation-requests"
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?
Summary
PR #48 fixed the initialize handshake — but a second protocol gap then dominated every codex turn:
mcpServer/elicitation/request. Codex 0.130 sends this server-initiated JSON-RPC request to ask permission for MCP tool calls (e.g.inbox_peek). Without a response, the turn wedges and times out at 600s.This PR makes
AppServerProcess._dispatchroute server-initiated requests (method + id) properly:set_inbound_request_handler) if one exists for the method.Production evidence — 2026-05-24
CR2 + Researcher post-PR#48 still produced zero
agent_logfor 15+ minutes because every cron tick hit this loop.Upstream contract
Per
codex-rs/app-server/README.md:For tool-call elicitations specifically the
_meta.codex_approval_kindis"mcp_tool_call". Since our MCP server is the in-processmoleculeadapter (we wrote it, trust it,approvalPolicy:neveris already set inthread/start), every elicitation should be auto-accepted.Fix
app_server.py: extend_dispatchwith an inbound-request branch. New publicset_inbound_request_handler(method, handler)API for callers that want custom per-method policy. Default policy: auto-acceptmcpServer/elicitation/request; decline (don't hang) anything else, with a WARN log.tests/mock_app_server.py: newsend_inbound_requesttest RPC that fires a server→client request and relays the client's response back so tests can assert on it.tests/test_app_server.py: 3 new tests:test_inbound_elicitation_request_auto_accepted(regression guard for the CR2/Researcher wire)test_inbound_unknown_request_auto_declined(no-hang policy)test_inbound_request_handler_override(custom handler reaches result)15/15 tests pass locally.
Test plan
pytest tests/test_app_server.py -x -q— 15/15 passagent_logcount > 0 within one cron tickRelated
🤖 Generated with Claude Code
Codex 0.130 introduced `mcpServer/elicitation/request` — a server-initiated JSON-RPC request that pauses an active turn and asks the client whether to allow a configured MCP server to invoke a specific tool. Without a response, the turn wedges and times out at 600s with `codex turn ... wedged: no events for 90s (deltas=N) — failing turn`. Observed live 2026-05-24 on CR2 + Researcher AFTER PR#48 landed: unrecognized message from app-server: {'method': 'mcpServer/elicitation/request', 'id': 0, 'params': {'threadId': '019e5981-...', 'turnId': '019e5981-...', 'serverName': 'molecule', 'mode': 'form', '_meta': {'codex_approval_kind': 'mcp_tool_call', 'persist': ['session', 'always'], 'tool_description': 'List pending inbound messages without removing them.', 'tool_params': {'limit': 10}, ...}, 'message': 'Allow the molecule MCP server to run tool "inbox_peek"?', 'requestedSchema': {...}}} codex turn 019e5981-2ea9-73a3-ad4f-104fc0886199 wedged: no events for 90s (deltas=34) — failing turn codex turn timed out after 600s PR#48 unblocked the initialize handshake — Registered:200, POST / 200 OK — but this second wedge then dominated every turn. Fix: AppServerProcess._dispatch now routes server-initiated REQUESTS (method + id) through: 1. A caller-registered handler (set_inbound_request_handler) if one exists for the method. Handler is async (method, params) -> result; result becomes the JSON-RPC `result` payload. Exceptions get surfaced as JSON-RPC -32000. 2. Default policy for unregistered methods: - mcpServer/elicitation/request -> {action: 'accept', content: {}} (our MCP server is the in-process molecule adapter — we wrote it, we trust it; auto-approving every elicitation matches the approvalPolicy=never config codex is started with) - everything else -> {action: 'decline', content: null} + WARN log (better to decline cleanly than hang the turn) Both paths use `asyncio.create_task` to keep the reader loop non-blocking during slow handlers. Tests: - mock_app_server.py: new `send_inbound_request` test RPC that fires a server→client request and relays the client's response back so tests can assert on it. Plus a _pending_inbound futures map routed from the read loop when the client's response comes back. - test_app_server.py: 3 new tests: - test_inbound_elicitation_request_auto_accepted (regression guard for the exact wire we hit on CR2/Researcher) - test_inbound_unknown_request_auto_declined (no-hang policy) - test_inbound_request_handler_override (custom handler works, receives method+params, return value reaches JSON-RPC result) 15/15 tests pass locally. This is the second of the two protocol gaps causing codex agents to wedge. PR#48 fixed the initialize handshake; this fixes the in-turn elicitation loop. Together they restore codex template productivity end-to-end. Tracking: internal#659 P1#1 follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>APPROVE.
Second protocol gap — PR#48 fixed initialize handshake, this fixes the in-turn mcpServer/elicitation/request loop that was producing the post-PR#48 wedge symptom "unrecognized message from app-server" + "codex turn ... wedged: no events for 90s".
Review points:
Resolves internal#659 P1#1 (part 2).