Skip to content

question tool blocks indefinitely in non-interactive contexts #936

Description

@sahrizvi

Problem

When altimate-code run "<task>" --yolo is invoked as a subprocess (Claude Code's Bash tool, CI, subprocess.run(..., stdin=None), agent harnesses), and a skill that uses the question tool fires during the run, the process hangs at 0% CPU forever. The harness eventually TaskStops the subprocess, looking identical to a deadlock.

Concretely surfaced during the data-parity skill's run when it detected PII columns in DimCustomer/DimEmployee and (per its safety rules) asked the user "May I run row-level hashdiff comparisons where sample diff rows may appear if mismatches exist?" with options "Approve row diff" / "Profile only". 70 seconds later, Claude Code TaskStop'd the subprocess.

Root cause

packages/opencode/src/tool/question.ts calls Question.ask(), which awaits an Effect Deferred. In TUI mode the deferred resolves when the user clicks an option. In claude --print / CI / non-interactive contexts, there is no TUI and no user — the deferred awaits forever, while the parent host waits for the subprocess to finish. Neither side has a path to give up.

Proposed fix

In non-interactive contexts (no TTY, or explicit env-var opt-in), auto-resolve question with a conservative-by-default policy and surface the choice clearly in the tool result so the calling LLM can adapt.

Resolution policy (env-var controlled, default safe):

  • Detect non-interactive: !process.stdin.isTTY, with overrides ALTIMATE_FORCE_INTERACTIVE=1 (use the original interactive Deferred path) and ALTIMATE_NON_INTERACTIVE=1 (force non-interactive even when isTTY is true).
  • ALTIMATE_AUTO_ANSWER=last (default in non-TTY) — pick the option whose label/description contains a safe keyword (skip, cancel, no, abort, profile only, decline, deny, stop); fall back to the last option (UX convention: safer/cancel typically sits at the end).
  • ALTIMATE_AUTO_ANSWER=first — always first option.
  • ALTIMATE_AUTO_ANSWER=skip — return Unanswered for all questions.
  • ALTIMATE_AUTO_ANSWER="<exact label>" — exact-match an option's label.

The tool result text explicitly flags non-interactive auto-answer ("Running in non-interactive mode (no TTY). Auto-answered with safe defaults: …"), so downstream agents can adjust strategy if needed.

Why not just always pick "cancel"?

Picking cancel/abort blindly fails open in the opposite direction: skills that ask permission to do reasonable work would always get a no, breaking the user's actual intent. The safe-keyword scan tries to match the question author's intent (these are typically "may I do destructive thing X?" prompts) without blocking legitimate flows.

Where this should live

Two choices:

  1. At the tool boundary (this fix) — packages/opencode/src/tool/question.ts short-circuits before calling Question.ask().
  2. At the Effect layer — push the non-interactive detection into Question.ask() itself.

Option 1 is the proposed PR. Option 2 is a deeper, more invasive change and may be the right long-term home — happy to revisit if reviewers prefer that path.

Test plan

  • Unit test (mocked TTY): non-interactive mode + 2-option question → assert tool result names the auto-answered option and tags it as non-interactive.
  • Unit test (ALTIMATE_FORCE_INTERACTIVE=1 + non-TTY): assert original Question.ask() path is taken (defers to interactive Deferred).
  • Unit test (ALTIMATE_AUTO_ANSWER=skip): assert Unanswered returned without calling Question.ask().
  • Smoke: trigger any skill that uses question from a Claude Code Bash tool call; expect completion in seconds, not a hang.

Context

Surfaced during the same multi-week experiment series that produced #934. Patched locally in altimate-code-preview (packages/opencode/src/tool/question.ts, ~70 lines + accompanying test/tool/question.test.ts); running clean against real workloads for ~9 days. Full writeup: plugin-skill-experiments/03-issues-and-fixes.md Issue #5.

PR incoming.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions