Skip to content

Clear Needs input when Claude moves past a prompt answered in the terminal - #14942

Closed
teamleaderleo wants to merge 8 commits into
mainfrom
fix/needs-input-lag
Closed

teamleaderleo wants to merge 8 commits into
mainfrom
fix/needs-input-lag

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Sep 27, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

After you answer a Claude Code permission prompt in the terminal, the sidebar kept showing "Needs input" next to "Running" for up to two minutes, and the Feed card for that prompt stayed actionable. The Feed now retires the prompt as soon as Claude's hooks show it moved on, so only "Running" remains.

Why it lingered. Claude shows its permission prompt as soon as the PermissionRequest hook starts, and when the user answers in the terminal it neither kills nor signals the hook. With Claude Code 2.1.283, a PermissionRequest hook that sleeps 60 s ran to completion after the prompt was answered, while PostToolUse and the next tool call went ahead. cmux's hook (cmux hooks feed --source claude) therefore stays parked in FeedCoordinator.ingestBlockingWithOutcome until its ~120 s deadline, and the Feed-owned overlay cmux.feed.attention:claude_code only clears when that wait ends. Claude's next PreToolUse sets the agent's own claude_code slot back to Running, so both rows show.

What changes. When the Feed accepts a later Claude hook from the same session and the same agent (main agent, or one subagent by agent_id), it retires that agent's pending decisions through the existing invalidation path: the hook returns neutral output, the card expires, the overlay and its banner clear, and a tool call journals the request as resolved.

  • The agent's next PreToolUse retires a prompt when either tool is one Claude never runs beside another (Edit, Write, MultiEdit, NotebookEdit), or the prompt is a Bash, AskUserQuestion or ExitPlanMode prompt, which come alone in practice. A prompt on a concurrency-safe tool (Read, WebFetch, MCP reads) can wait while a sibling runs, so a sibling call does not retire it. A read-only Bash can run beside read-only siblings, so a sibling that starts while its prompt waits retires it early; the agent's own status slot already flips to Running in that case.
  • The end of that agent's turn (Stop, UserPromptSubmit, SubagentStop) retires any of its prompts; SessionEnd retires every prompt in the session.
  • The tool call that raised a prompt never retires it. It normally reaches the Feed first (the decision hook waits for earlier queued hooks, and same-session Feed ingress is ordered); if it arrives late, the first matching call is consumed once.
  • PreToolUse Feed telemetry now carries agent_id (also kept in the compact queued-hook fallback), so a background subagent's work can't clear the main agent's prompt.
  • A subagent's prompts now use their own request ID as their journal identity instead of the shared agent_id, so resolving one prompt no longer marks that subagent's later prompts stale.

Only claude is affected; other agents' Feed decisions are unchanged.

Related: #14493 picks one row per panel by timestamp. On its own it would show only "Needs input" after the answer, because an unchanged Running value keeps its first timestamp; with this change the overlay is gone by then, so the two compose (#14493 covers the seconds before Claude's own notification during a real wait).

Testing

  • Regression (cmuxTests/FeedDecisionAnsweredInTerminalTests.swift), red first: on the test-only commit e57362d the changed-suites app-host lane ran it and claudeResumingWorkRetiresPendingPermissionAndNeedsInput failed with the overlay still "Needs input", the lifecycle still .needsInput, the request still awaiting and the card not expired (job, RATCHET_NEW_FAILURE). The subagent guard test passed there.
  • Green: at 7757cd0 (head) every check passed (56, including the 5 required). The changed-suites lane ran all nine tests in this suite and they passed, with the rest of the selected batch (489 typed test cases, including AgentHookDeliveryQueueTests and the Claude hook suites) (job); macOS compile admission and CLI product tests passed. Earlier CLI product tests failures were owned-mini capacity refusals at "Set up runner" ("the gui token is taken"), not test runs.
  • Also covers: a sibling Read doesn't retire a pending Read prompt, an Edit after a pending Read retires it, the next edit of the same file retires an answered Edit prompt, the late raising call is consumed once, Stop retires a WebFetch prompt, Stop doesn't retire a subagent's prompt but SessionEnd does, and a subagent prompt's journal identity is its request ID.
  • scripts/ci/guards-local.sh (Linux group, 66 steps) passed on the merge with main. No local app build (not allowed on this machine).
  • Localization: no new user-facing strings.

Dogfood

Fleet build: pr-14942-needs-input-v3, job 61602660edd7ae93240ba10a on 077b878 (done on cmuxs-Mac-mini, artifact sha256:dff79251f829fd7aebd79eceda9a4fde0d3a68c79bcd9fc4a3a982a91e9e186f; later heads only merge main). Built with --backend-mode local because cmux-dev-backend-1 doesn't resolve from the submitting Mac, so publish-hq refuses the artifact; the change doesn't touch backend paths. Baseline for the old behavior: pr-14942-needs-input-base-v1, job 370b8dac484c2be78c2914cf (test-only commit e57362d, artifact sha256:f901b81b…).

Not yet run: GUI dogfood on a fleet Mac is blocked from this session. Checks still to run, in a terminal of the tagged app (cmux DEV pr-14942-needs-input-v3), with a real claude (or the replay script below, which runs the exact hook commands from cmux hooks claude inject-settings in Claude's order):

  1. Ask Claude to run a write command (touch /tmp/x). When the prompt shows, the sidebar row shows Needs input. Answer 1 in the terminal. Expect the row to drop Needs input within a second of Claude's next tool call, leaving only Running; the Feed card shows expired.
  2. Same, but let Claude's answered tool be the last of the turn. Expect Needs input to clear at Stop.
  3. Answer a prompt from the Feed card instead. Expect the existing behavior (card resolved, overlay cleared).
  4. Ask for two parallel Reads, one outside the project. While that prompt waits, the sibling read must not clear Needs input.
  5. With a background subagent running, answer nothing on the main agent's prompt and confirm the subagent's tool calls leave Needs input lit.
  6. Baseline: repeat 1 on pr-14942-needs-input-base-v1; Needs input stays next to Running until about two minutes after the prompt appeared.
Replay script
#!/usr/bin/env python3
"""Replay Claude Code's hooks for a permission prompt answered in the terminal.

Runs the exact hook commands this cmux build injects into Claude
(`cmux hooks claude inject-settings`), with the payloads Claude sends, in the
order Claude 2.1.283 fires them: the PermissionRequest hook keeps waiting after
the user answers, and the next tool call's PreToolUse follows.
Run it inside a terminal of the cmux build under test.
"""
import json
import os
import subprocess
import sys
import time
import uuid

CLI = os.environ.get("CMUX_BUNDLED_CLI_PATH") or "cmux"
SETTINGS = json.loads(subprocess.check_output([CLI, "hooks", "claude", "inject-settings"]))
HOOKS = SETTINGS["hooks"]
SESSION = str(uuid.uuid4())
ENV = dict(os.environ, CMUX_CLAUDE_HOOK_CMUX_BIN=CLI, CMUX_CLAUDE_PID=str(os.getpid()))
START = time.monotonic()


def say(text):
    print(f"[{time.monotonic() - START:6.1f}s] {text}", flush=True)


def fire(event, wait=True, **fields):
    payload = {
        "session_id": SESSION,
        "hook_event_name": event,
        "cwd": os.getcwd(),
        "transcript_path": "",
        "permission_mode": "default",
        **fields,
    }
    procs = []
    for group in HOOKS.get(event, []):
        matcher = group.get("matcher", "")
        if matcher and matcher != fields.get("tool_name"):
            continue
        for hook in group["hooks"]:
            if hook.get("async"):
                continue
            proc = subprocess.Popen(
                ["/bin/sh", "-c", hook["command"]],
                stdin=subprocess.PIPE,
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
                env=ENV,
            )
            proc.stdin.write(json.dumps(payload).encode())
            proc.stdin.close()
            procs.append(proc)
    if wait:
        for proc in procs:
            proc.wait()
    return procs


pause = float(sys.argv[1]) if len(sys.argv) > 1 else 8.0
say("SessionStart + UserPromptSubmit")
fire("SessionStart", source="startup")
fire("UserPromptSubmit", prompt="touch a file")
say("PreToolUse Bash: Claude starts a tool call (sidebar: Running)")
fire("PreToolUse", tool_name="Bash", tool_use_id="toolu_dogfood_1",
     tool_input={"command": "touch /tmp/cmux-dogfood.txt", "description": "Create a file"})
say("PermissionRequest Bash: Claude shows its prompt, the hook waits on the Feed (sidebar: Needs input)")
waiting = fire("PermissionRequest", wait=False, tool_name="Bash",
               tool_input={"command": "touch /tmp/cmux-dogfood.txt", "description": "Create a file"})
time.sleep(pause)
say("User answers 1 (Yes) in the terminal. Claude runs the tool; its hook keeps waiting")
fire("PreToolUse", tool_name="Read", tool_use_id="toolu_dogfood_2",
     tool_input={"file_path": "/tmp/cmux-dogfood.txt"})
say("PreToolUse Read: Claude is on its next tool call (sidebar should show only Running)")
for second in range(int(pause * 2)):
    time.sleep(1)
    if all(proc.poll() is not None for proc in waiting):
        say("PermissionRequest hook returned")
        break
else:
    say("PermissionRequest hook is still waiting on the Feed")
time.sleep(pause)
say("Stop: turn ends")
fire("Stop", stop_hook_active=False)
for proc in waiting:
    if proc.poll() is None:
        proc.terminate()
say("done")

Changelog

Fixed: After you answer a Claude Code permission prompt in the terminal, the sidebar drops Needs input as soon as Claude moves on instead of showing it next to Running for up to two minutes, and the Feed card for that prompt expires

Checklist

  • Behavior changes have added or updated tests
  • Reviewed with a subagent before merge (two passes; findings fixed)

🤖 Generated with Claude Code

Claude shows its own permission prompt while the PermissionRequest hook
waits on the Feed, and it lets that hook run on after the user answers in
the terminal. The Feed wait, card, and sidebar Needs input overlay then
stay up next to Running until the hook's deadline. These tests fail until
Claude's next hook retires the answered request.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 27, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

teamleaderleo and others added 4 commits September 27, 2026 04:09
Claude shows its permission prompt while the PermissionRequest hook waits
on the Feed and lets the hook run on after the user answers in the
terminal, so the Feed wait, card, and Needs input overlay stayed up beside
Running until the ~120 s deadline. When the Feed accepts Claude's next
tool call (for a prompt on a tool that never runs beside another), or the
end of that agent's turn or session, it now retires the request: the hook
returns neutral output, the card expires, and the overlay and banner clear.

Work is matched by agent: PreToolUse telemetry now carries agent_id (also
kept in the compact queued-hook fallback), and a subagent's prompts use
their own request ID rather than agent_id as their journal identity so a
resolved prompt no longer marks that subagent's later prompts stale.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A prompting tool can wait while a concurrency-safe sibling runs, so a later
tool call only proves the answer when either call is one Claude never runs
beside another (Edit, Write, MultiEdit, NotebookEdit), or the prompt is a
Bash, question, or plan, which prompt alone in practice. The tool call that
raised a request is now consumed once instead of matched by content, so the
next edit of the same file retires an answered Edit prompt.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Catch-up merge by scripts/ci/catch_up_pr.py (RFC #14631).
Merged by scripts/merge-main.sh: origin/main at ef8b037.

Catch-up-previous-head: 4ab2ac3
Catch-up-base: ef8b037
@github-actions

github-actions Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

CI failure attribution

CI passes on 7757cd0d3b (run 36322340083 attempt 2).

Written by scripts/ci/classify_failures.py (ci-failure-attribution.yml); signatures are its SIGNATURES table. A machine verdict is the runner's fault, not this PR's.

teamleaderleo and others added 3 commits September 27, 2026 05:22
cancelNotification is file-private to FeedCoordinator.swift, so the
retirement calls the existing invalidateSemanticRequest instead of a moved
copy. Its snapshot already excludes replied and timed-out requests, and a
racing reply's journal entry and notification clear are idempotent.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Drops this branch's CHANGELOG.md line: release notes now come from the PR's Changelog section (#14934).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Catch-up merge by scripts/ci/catch_up_pr.py (RFC #14631).
Merged by scripts/merge-main.sh: origin/main at f5c179f.

Resolved generated files:
- cmux.xcodeproj/project.pbxproj: union of added entries, then normalize-pbxproj.py

Catch-up-previous-head: 077b878
Catch-up-base: f5c179f
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Closing as superseded by #15170, which fixes the same stale "Needs input" overlay after a Claude permission prompt is answered in the terminal: a later same-session, same-agent Claude hook retires the pending Feed decision, so the overlay and card clear without waiting for the hook timeout. Any remaining edge cases from this draft (per-tool sibling rules, subagent journal identity) can be raised on #15170.

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