Skip to content

Claude: user-input answers reach AskUserQuestion unvalidated, so an {answers: [...]} value fails after the request is consumed #13398

Closed

Description

@Luzivog

Summary

thread.user-input.respond accepts answers: Record<string, unknown> (ProviderUserInputAnswers in packages/contracts/src/orchestration.ts). The Codex runtime normalizes each value (a string, a string array, or { answers: string[] }) in toCodexUserInputAnswer (apps/server/src/provider/Layers/CodexSessionRuntime.ts) and fails with CodexSessionRuntimeInvalidUserInputAnswersError for anything else. The Claude adapter does neither: handleAskUserQuestion in apps/server/src/provider/Layers/ClaudeAdapter.ts returns the value unchanged as updatedInput.answers.

Claude Code's AskUserQuestion schema requires answers: Record<string, string>; it accepts a string array and joins it with ", ". So { "answers": ["..."] }, which the Codex path accepts, fails inside Claude Code:

The permission handler returned updatedInput for AskUserQuestion that failed schema validation: AskUserQuestion failed due to the following issue:
The parameter `answers.<question text>` type is expected as `string` but provided as `unknown`

By then T3 has removed the pending request and emitted user-input.resolved. The respond command succeeded, the question is no longer pending, and the model gets a tool error instead of the answer. The web UI sends strings and string arrays, so only API clients hit this, but the command contract allows it.

Steps to reproduce

  1. In a Claude thread, ask the model to call AskUserQuestion with one single-select question.
  2. Dispatch thread.user-input.respond with answers: { "<question text>": { "answers": ["Blue"] } }.
  3. The dispatch succeeds. The AskUserQuestion tool call fails with the error above, and the request is no longer pending.

Expected

One answer contract for every provider. Normalize answers once, for example in ProviderService.respondToUserInput next to appendUserInputAttachmentPaths, to string | string[] (accepting { answers: string[] }). Reject anything else with a ProviderValidationError before the pending request is consumed, so the client can send a valid answer.

Versions

  • T3 Code 0.0.43-nightly.20260924.2187 (78af372). The code is the same on main.
  • Claude Code 2.1.280 and 2.1.281, started by the Agent SDK.

Activity

  1. juliusmarminge commented on Sep 24, 2026

    @juliusmarminge
    Member

    Confirmed. thread.user-input.respond accepts answers: Record<string, unknown> (ProviderUserInputAnswers in packages/contracts/src/orchestration.ts). Codex normalizes each value in toCodexUserInputAnswers (apps/server/src/provider/Layers/CodexSessionRuntime.ts) before it drops the pending request: a string, a string array, or { answers: string[] }. Anything else fails with CodexSessionRuntimeInvalidUserInputAnswersError and the question stays open.

    Claude does not. ClaudeAdapter.respondToUserInput deletes the pending entry and completes the deferred with the raw record. handleAskUserQuestion then emits user-input.resolved and returns that record as updatedInput.answers. Claude Code validates AskUserQuestion only after that callback returns. Its answers are strings keyed by question text (a string array is joined with ", "). { answers: ["Blue"] } is an object, so the tool errors with the schema message in the report. The respond command has already succeeded, and derivePendingRequests treats user-input.resolved as terminal, so the client cannot send a corrected answer.

    First-party clients are unaffected. Web (buildPendingUserInputAnswers) and mobile send string | string[] only. This is an API-client hole: the wire schema allows the Codex object form on every provider.

    Normalizing once in ProviderService.respondToUserInput, beside appendUserInputAttachmentPaths, is the right place. Unwrap { answers: string[] } to string | string[], and fail any other value with ProviderValidationError before adapter.respondToUserInput. A non-stale provider.user-input.respond.failed leaves the question open, so the client can retry. Keep string arrays as arrays: multi-select answers from the UI are arrays, and Codex already wraps those.

    The same object is not handled uniformly elsewhere, so a Claude-only coerce would leave the contract split:

    • Antigravity rejects an unusable answer before settling the deferred, so the question stays open.
    • Grok's answerValues only reads strings and string arrays, and it resolves the question before building the provider response, so the object is dropped.
    • OpenCode's toOpenCodeQuestionAnswers turns the object into an empty answer list and then replies.
    • Cursor returns the raw record from cursor/ask_question.
  2. added
    acceptedfeature request accepted
    bugSomething is broken or behaving incorrectly.
    via-triageFiled through npx t3 triage
    on Sep 24, 2026
  3. juliusmarminge commented on Oct 20, 2026

    @juliusmarminge
    Member

    Note

    Grok responding on behalf of Julius.

    Fixed by #14884, which just landed on main. The orchestrator now validates user-input answers before the request resolves: { answers: string[] } values are unwrapped to a string list, and malformed values (objects, null, numbers, mixed arrays) are rejected while the request stays pending, so the client can retry with a valid answer instead of the provider failing after the request is consumed.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    acceptedfeature request acceptedbugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions