Skip to content

The stall wake-nudge types text and presses Enter into the pane, so a session parked on a permission prompt gets that prompt answered #739

Description

@pbean

Found while validating #727, and not filed anywhere. Validated against main @ cf5d5b99; every cite below was resolved by symbol.

The mechanism

A nudge is delivered as keystrokes plus Enter, not as a message on a side channel. GenericAdapter.send_text (src/bmad_loop/adapters/generic.py:1083-1084) forwards to the multiplexer, and BaseTmuxBackend.send_text (src/bmad_loop/adapters/tmux_base.py:358-361) is:

def send_text(self, window_id: str, text: str) -> None:
    self._tmux("send-keys", "-t", window_id, "-l", text)
    time.sleep(0.3)  # let the TUI ingest the paste before submitting
    self._tmux("send-keys", "-t", window_id, "Enter")

PsmuxMultiplexer does not override it, so both tmux-transport backends behave this way. (OpencodeHttpAdapter.send_text, adapters/opencode_http.py:687-697, posts over HTTP instead and is not affected.)

That is correct and necessary when the pane holds an agent waiting for input. It is not correct when the pane holds something else.

Why a permission prompt is exactly the state that trips it

The wake nudge fires on silence, and a session parked on a permission prompt is silent by construction — it has rendered its confirmation screen and is waiting, so the pane log stops growing.

  • STALL_NUDGE_TEXT (generic.py:133-141) is sent at generic.py:903, inside the branch at generic.py:891-911, when the pane log has not grown for _stall_grace_s.
  • _stall_grace_s / _stall_nudges come from _configure_dev_knobs (generic.py:1289-1299): limits.dev_stall_grace_s (default 600 s) and limits.dev_stall_nudges (default 2 per silence), bounded per session by limits.dev_stall_nudges_cap (default 6) — policy.py:113-131.
  • The grace re-arms on pane growth (generic.py:886-890), which is the guard that keeps a working session from being nudged. A parked prompt produces no growth, so the grace expires and the nudge is typed into the prompt.

So the nudge is not merely able to land on a prompt; the prompt's own stillness is what summons it.

What lands

send-keys -l types the nudge text literally into whatever has focus, then Enter submits. All four nudge strings begin with the word "You" — NUDGE_TEXT (generic.py:123), STALL_NUDGE_TEXT (:133), BUDGET_NUDGE_TEXT (:145), CONTRACT_NUDGE_TEXT (:161) — so the first character reaching a confirmation prompt is Y, followed eventually by Enter.

What that actually does is CLI- and prompt-specific and I am not going to claim a single outcome across five profiles: a y/n prompt reads it as an accept, a numbered menu may ignore the letters and have Enter take the highlighted default, and a text-input prompt may swallow the whole sentence. All three are bad, and none of them is a decision bmad-loop intended to make. An unattended loop answering a permission prompt on the operator's behalf, by accident, is the concerning shape regardless of which branch it takes.

Nothing in the orchestrator knows prompts exist

A sweep of src/bmad_loop/**/*.py (run in Python, not shell grep) for any permission-prompt concept returns nothing beyond the bypass_args template comments in policy.py/runsetup.py and the unrelated per-story-spec-approval gate mode. There is no detection, no guard, and no path that treats "a prompt is on screen" differently from "the agent is thinking".

Reachability is not hypothetical

#727 captured a claude session sitting on a confirmation screen with tokens: 0 and a ~1,930-byte pane log containing only the menu. That is precisely the input state described above. Two ways in that are already known: an operator whose bypass flags were dropped (#740adapter.extra_args replacing rather than extending bypass_args), and a CLI that prompts for something the bypass flags do not cover.

What is being asked for

Not removal of the nudge. It is the re-invocation bmad-loop otherwise lacks, and the comment at generic.py:128-132 explains why it has to exist — a turn ended to await a slow background process would otherwise wait forever.

The narrow ask is that the nudge stop being able to answer a question. Some directions, in rough order of cheapness:

  1. Do not send the trailing Enter when the pane's last screen looks like a prompt rather than an idle agent. The nudge text still lands, visible to a human attaching later, but nothing is submitted.
  2. Detect the prompt and take a different path — stall out, or escalate, rather than nudge. This overlaps A dev session waiting on an interactive permission prompt is recorded as crashed, and retry burns the last attempt #727's suggestion 3, which is the open design question there.
  3. Probe for the prompt before the run rather than during it, which is A dev session waiting on an interactive permission prompt is recorded as crashed, and retry burns the last attempt #727's suggestion 2 (probe._probe_argv, probe.py:566, drives a content-free turn with the same bypass_args a run would).

Two channels are explicitly not the answer, both settled on #727:

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Real defect - workaround exists or blast radius is narrowarea:adaptersCoding-CLI adapters and profilesbugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions