Did your last agent run freeze with no stack trace?
You blamed the model, then you reran the same prompt.
Silence is not a reasoning bug by default.
I keep this FAQ beside my agent command transcripts.
The same five claims appear after almost every mysterious hang.
Each myth has a shell check you can run today.
What I mean by a hang
The child stays in interruptible sleep and burns no CPU.
The log stops, and the wrapper never receives an exit status.
The agent loop then looks dead while something waits on I/O.
Does that match your last "the model got stuck" story?
Myth 1: The model hung, so the command hung
The claim is simple, common, and usually wrong.
People treat a quiet chat as a quiet inference loop.
The freeze often lives in a child syscall instead.
I collect process state before I touch the prompt again.
# Replace AGENT_PID with the wrapper that launched the command.
ps -o pid,ppid,stat,wchan:32,etime,cmd --ppid "$AGENT_PID"
Read STAT and wchan before you spend another retry.
Sleeping with pipe_read is not a stalled token stream.
It is a blocking read, which is a different failure class.
The corrected mental model is blunt and testable.
Assume I/O wait until ps proves something else.
Why would a tokenizer sit in pipe_read anyway?
Myth 2: Non-interactive shells already skip every prompt
The claim sounds reasonable if you live in CI.
People assume agent shells export the same guard variables.
Many tools still key off a TTY, CI, or both.
I print the contract before I blame the test suite.
printf 'CI=%s\n' "${CI-<unset>}"
printf 'TERM=%s\n' "${TERM-<unset>}"
printf 'PAGER=%s\n' "${PAGER-<unset>}"
printf 'GIT_PAGER=%s\n' "${GIT_PAGER-<unset>}"
python - <<'PY'
import sys
for name in ("stdin", "stdout", "stderr"):
stream = getattr(sys, name)
print(name, "isatty=", stream.isatty(), "encoding=", stream.encoding)
PY
CI runners usually export CI=true without extra thought.
Agent wrappers often forget that one cheap environment line.
Then git opens less, and npm asks a question.
The corrected model is a contract you set yourself.
Non-interactive behavior is not a kernel feature.
Have you watched less wait for q with no keyboard?
export CI=true
export PAGER=cat
export GIT_PAGER=cat
export npm_config_yes=true
export DEBIAN_FRONTEND=noninteractive
git config --global core.pager cat
Those exports are a starting contract, not a personality transplant.
A tool can still call input() after you set CI=true.
Did you verify the binary, or only the environment file?
Myth 3: A Python one-liner proved the suite is headless
The claim is that isatty() on stdout tells the whole story.
People check one stream and then trust every other descriptor.
stdin, stdout, and stderr can disagree on the same process.
I check all three before I call the suite headless.
python - <<'PY'
import sys, os
for fd, name in ((0, "stdin"), (1, "stdout"), (2, "stderr")):
print(name, "isatty", os.isatty(fd), "fd", fd)
print("stdin_isatty", sys.stdin.isatty())
PY
Piped stdout can still sit beside a live stdin TTY.
The reverse also happens on many agent wrappers.
input() then blocks even when logs look fully piped.
The corrected model treats each file descriptor separately.
One green isatty check is not a hang vaccine.
Would you trust a test that never inspects stdin?
Myth 4: Timeout wrappers explain the hang and fix it
The claim is that timeout 30 turns mystery into signal.
People add a wrapper, catch 124, and call the bug solved.
A killed process still has no named root cause.
I use timeout as a bound, not as a diagnosis.
timeout --signal=TERM --kill-after=2s 15s ./run_tests.sh
echo "timeout_exit=$?"
# 124 means the clock fired. It does not name the waiter.
Exit 124 only says the clock won the race.
It does not say pager, input(), or a forgotten yes flag.
You will retry the same hang on the next loop.
The corrected model records why the read blocked.
Kill the waiter only after you name the waiter.
Otherwise the next agent pass repeats the stall.
Myth 5: A remote Linux box behaves like your laptop TTY
The claim is that Linux is Linux, so prompts match.
People copy a local command into a remote agent shell.
The remote command has no keyboard, and often no TERM.
I treat laptop TTY and agent host as different machines.
When I need that split, I park the hang lab remotely.
I use MonkeyCode's free model access and free server option.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
Those two availability claims are the only product facts here.
I am not assuming extra hardware details, quotas, or model names.
The point is isolation from an interactive local shell.
# On the remote shell the agent actually uses:
tty || true
echo "TERM=${TERM-<unset>}"
stty -a 2>/dev/null || echo "no controlling terminal"
tty saying not a tty should change your test plan.
Color, pagers, and confirmation prompts all key off that.
Your laptop hid those branches every single day.
Docker adds a third TTY story on the same host.
A container without -t has no terminal either.
-i without -t still leaves isatty() false.
# Proposal only: compare descriptor truth inside a throwaway container.
docker run --rm python:3.12-slim python -c "import sys; print('stdin', sys.stdin.isatty(), 'stdout', sys.stdout.isatty())"
docker run --rm -i python:3.12-slim python -c "import sys; print('stdin', sys.stdin.isatty(), 'stdout', sys.stdout.isatty())"
The corrected model is a two-host picture.
Laptop TTY is a privilege the agent host may lack.
Why copy an interactive command into a headless shell?
Artifact: a fifteen-minute hang lab
This lab is a proposal you can run locally or remotely.
I am not reporting production timings from a real fleet.
Label it as a fixture, not as a benchmark.
Create hanglab/ with three tiny files.
mkdir -p hanglab
cat > hanglab/wait_for_input.py <<'PY'
import sys
print("about to read stdin", flush=True)
line = sys.stdin.readline()
print("got:", repr(line), flush=True)
PY
cat > hanglab/maybe_pager.sh <<'SH'
#!/bin/sh
set -eu
# Some git builds still consult a pager when stdout looks like a TTY.
git --no-pager log -1 >/dev/null 2>&1 || true
printf 'long output\n%.0s' $(seq 1 200) | ${PAGER:-less}
SH
chmod +x hanglab/maybe_pager.sh
cat > hanglab/run.sh <<'SH'
#!/bin/sh
set -eu
mode=${1:-input}
case "$mode" in
input)
python hanglab/wait_for_input.py
;;
pager)
hanglab/maybe_pager.sh
;;
ci)
export CI=true
export PAGER=cat
export GIT_PAGER=cat
export npm_config_yes=true
python hanglab/wait_for_input.py < /dev/null
;;
*)
echo "usage: $0 input|pager|ci" >&2
exit 2
;;
esac
SH
chmod +x hanglab/run.sh
Run three comparisons and write the results down.
Do not trust memory. Trust the recorded exit codes.
# 1) This should block until you type a line.
python hanglab/wait_for_input.py
# 2) This should exit fast with EOF on stdin.
python hanglab/wait_for_input.py < /dev/null
echo "eof_exit=$?"
# 3) Bound the pager case so it cannot eat the session.
timeout 8s hanglab/maybe_pager.sh
echo "pager_exit=$?"
# 4) The contract path should return without a keyboard.
hanglab/run.sh ci
echo "ci_exit=$?"
Decision table I actually use
| Observation | Do not conclude | Conclude instead | First fix |
|---|---|---|---|
Quiet chat, child S plus pipe_read
|
Model stalled | Blocking read | Inspect stdin and pager |
timeout exit 124 |
Root cause found | Clock won | Name the waiter, then bound |
| stdout is not a TTY | Whole process is headless | Only stdout is piped | Check stdin and stderr too |
| Works on the laptop | Agent host is broken | TTY privilege differed | Set CI and PAGER=cat
|
CI unset in printenv
|
Tooling is flaky | Contract was never set | Export the CI guards |
Container without -t
|
Image is defective | No PTY was allocated | Decide if you need -t
|
I follow this order when a command goes quiet.
- Capture
psSTATandwchanfor the child. - Print
CI,TERM,PAGER, and the threeisattyflags. - Replay the command under
timeoutwith stdin from/dev/null. - Re-run with
PAGER=catandCI=trueas one change. - Keep the first setting that turns a hang into an exit code.
What this workflow is not
This is not a profiler for infinite loops in your code.
Busy R state with rising CPU is a different FAQ.
Do not use these checks for GPU hangs or kernel deadlocks.
Skip this approach if your product is the TTY itself.
Interactive debuggers, full-screen TUIs, and real serial consoles need a terminal.
A headless agent host is the wrong lab for those tools.
I also do not treat a free remote server as a secret vault.
Hang labs should use fake prompts and throwaway fixtures.
If a command must read a real secret, keep it off that host.
Limitations I will not hand-wave
isatty lies under some allocators and some CI wrappers.
script and pty.spawn can fake a TTY you did not want.
timeout cannot interrupt an uninterruptible disk wait.
Clock bounds are not diagnosis by themselves.
Killing less does not document why less started.
Your next agent retry will summon it again without a named fix.
I am not claiming any model quality ranking here.
Free model access only means I can rerun the lab without mixing TTYs.
It does not prove the suite is correct, fast, or complete.
Who should ignore this FAQ entirely?
People shipping ncurses products, hardware consoles, or true interactive REPLs.
People whose hang is a livelock with hot CPUs and growing logs.
The mental model I want you to keep
Hangs are usually waits, not silent intelligence failures.
Name the waiter, then set the contract, then bound the clock.
Ask what is blocked before you spend another prompt.
If you already have a MonkeyCode workspace, try the hang lab there.
Run hanglab/run.sh ci once and save the transcript.
Keep that transcript beside your next agent retry.
Top comments (0)