From 61f94785715e89930e514eeceea3485a05e45e4b Mon Sep 17 00:00:00 2001 From: Eric Lee Date: Tue, 21 Jul 2026 07:50:22 -0700 Subject: [PATCH] fix(headless): ask_user returns an explicit autonomous-proceed signal, not bare empty strings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In non-interactive mode there is no user to answer AskUserQuestion. The prior _noop_ask_user collapsed every question to "" — which gave the model no signal about WHY the answer was empty. Observed live (terminal-bench raman-fitting, clawcodex opus run): the agent asked two clarifying questions, got empty answers, then flailed through 23 curve-fit attempts and hit the agent timeout instead of committing to an approach. Return an explicit "no interactive user is available; proceed autonomously with your best judgment; do not ask again" answer so the model moves on decisively. Scope note: this is a headless-decisiveness quality fix, NOT a cure for AgentTimeoutError broadly — analysis of the opus run shows 7/9 timeouts are task-inherent (the same genuinely-long tasks — CompCert compile, QEMU boots, fasttext training, with 2400-3600s per-task timeouts — also time out under claude-code; counts 9 vs 10 are at parity). Co-Authored-By: Claude Fable 5 --- src/entrypoints/headless.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/entrypoints/headless.py b/src/entrypoints/headless.py index 44eaf33f..7b971777 100644 --- a/src/entrypoints/headless.py +++ b/src/entrypoints/headless.py @@ -853,12 +853,25 @@ def handler(request: PermissionAskRequest) -> PermissionAskReply: return handler +_NON_INTERACTIVE_ANSWER = ( + "No interactive user is available (running headless/non-interactive). " + "Proceed autonomously with your best judgment and reasonable default " + "assumptions; do not ask again." +) + + def _noop_ask_user(questions): # type: ignore[override] - # In non-interactive mode, collapse every question to an empty answer. + # Non-interactive mode: there is no user to answer. Returning bare + # empty strings left the model with no signal about WHY the answer was + # empty — observed live (terminal-bench raman-fitting) to make it flail, + # re-asking / retrying instead of committing to an approach. Return an + # explicit "proceed autonomously" answer so the model moves on + # decisively. (The interactive TUI still shows the real dialog; only the + # headless surface — which cannot collect input — substitutes this.) answers: dict = {} for q in questions or []: if isinstance(q, dict) and isinstance(q.get("question"), str): - answers[q["question"]] = "" + answers[q["question"]] = _NON_INTERACTIVE_ANSWER return answers