Summary
A contributor relay recovers from a retryable API error by typing try again into the
agent's pane. It correctly refuses to do that while a human is sitting at the pane β
nobody wants a watchdog typing over someone mid-keystroke. It decides whether a human
is there by asking tmux for #{client_activity}.
That field does not mean "a human typed." It means "this client sent us bytes," and
a terminal emulator sends bytes for reasons that have nothing to do with a person:
replies to the escape sequences the running application writes (capability queries,
colour queries, cursor-position reports), and β where enabled β mouse and focus events.
A Claude Code TUI issues such queries on its own schedule, so an attached but unused
terminal tab keeps client_activity advancing indefinitely.
The consequence is that automatic recovery never runs for the most ordinary setup there
is. just contribute-hive <backend> local prints an attach hint, contributors leave the
session attached in a terminal tab to watch it, and from then on every retryable API
error parks the task until a person notices and bumps it by hand. The relay logs its
reasoning each time, so the failure is at least visible β but it never resolves.
Blast radius: interactive mode, any backend, any contributor whose tmux session is
attached. Work is not lost, but a task stalls until a human intervenes, which is exactly
the unattended-operation property the retry logic exists to provide.
Observed, 2026-09-02
Contributor Danathar, claude backend, working issue #5681. The agent hit
β API Error: Connection lost mid-response. β which classifies correctly, it is in
TRANSIENT_API_ERROR_PATTERNS β and then:
[11:07:47] Task ct-β¦-5681-β¦ stopped on a retryable API error; someone is active on hive-claude-7645 (last input 136s ago), so not typing a retry
[11:09:47] Task ct-β¦-5681-β¦ stopped on a retryable API error; someone is active on hive-claude-7645 (last input 75s ago), so not typing a retry
[11:11:47] Task ct-β¦-5681-β¦ stopped on a retryable API error; someone is active on hive-claude-7645 (last input 195s ago), so not typing a retry
[11:13:47] Task ct-β¦-5681-β¦ stopped on a retryable API error; someone is active on hive-claude-7645 (last input 45s ago), so not typing a retry
Four consecutive ticks over six minutes, each recomputing a fresh "last input" age. The
contributor confirms they never typed into that pane β it is a background tab in their
terminal emulator. Working back from the log, client_activity advanced at 11:05:31,
11:08:32 and 11:13:02: irregular gaps of ~3min and ~4.5min, which is not what human
typing looks like and not what a fixed poll looks like either.
Both of the obvious explanations are ruled out on this host:
$ tmux show -g mouse
off
$ tmux show -gv focus-events
off
$ tmux list-clients -t hive-claude-7645 -F 'client=#{client_name} activity=#{client_activity} flags=#{client_flags}'
client=/dev/pts/1 activity_epoch=1788361982 flags=attached,focused,UTF-8
No mouse reporting, no focus events β and client_activity still moves. What remains is
the terminal answering queries the application asked it.
HUMAN_PRESENCE_IDLE_MS is 5 minutes and the observed gaps are shorter than that, so
active never went false and the 5-minute window never elapsed. Detaching the client
(tmux detach-client) is what released it.
Cause
bin/contributor-relay.sh:
function tmuxSessionHumanPresence() {
const out = execSync(`tmux list-clients -t ${TMUX_SESSION} -F '#{client_activity}' β¦`);
β¦
const idleMs = Math.max(0, Date.now() - newestSec * 1000);
return { attached: true, active: idleMs < HUMAN_PRESENCE_IDLE_MS, idleMs };
}
and its consumer in handleTransientAPIError:
const presence = tmuxSessionHumanPresence();
if (presence.active) {
β¦ send blocked_on_human; return; // no retry, ever, while this holds
}
#5277 already made the right distinction once β an attached client is not a person, so
attached alone must not block recovery. This is the same lesson one level down:
client_activity is not a person either. The refinement that fixed #5277 replaced one
proxy with a slightly better proxy rather than with a measurement of what was actually
meant.
Worth noting the log line's wording, last input Ns ago, states the assumption as fact.
It is what made this hard to see: the message asserts a human typed, so the natural
reading is to doubt the contributor rather than the field.
Fix shape
There is no tmux field that means "a human pressed a key," so the honest options are:
- Measure what actually matters: does the pane's input line change? A person at the
pane is composing something, so the CLI's input area differs from empty between ticks.
The relay already captures the pane every tick for other purposes, so this costs
nothing new and is a direct observation rather than a proxy.
- Require a much longer quiet window before treating activity as human β the
observed gaps are minutes apart, so a threshold above the largest plausible query
interval would clear it. This is tuning a proxy that is wrong in kind, and would
still misfire on a terminal that polls more often.
- Let the operator say so. An explicit
HIVE_ASSUME_UNATTENDED=1 for contributors
who never intend to type. Escape hatch, not a fix, and it puts the burden on the
person least placed to know the mechanism.
- Cap the deferral. Whatever the presence signal says, stop deferring after N ticks
and retry anyway: a task parked forever on a signal the relay cannot verify is worse
than one keystroke landing next to a human's. This is the smallest change that bounds
the damage, and composes with any of the above.
(1) plus (4) looks right: observe the thing that actually indicates composition, and
refuse to defer indefinitely on any presence signal.
Regression coverage: a fixture where client_activity advances on every tick with no
change to the pane's input line, pinned as not active β today's tests can only exercise
the proxy, which is why this passed.
Related: #5277 (attached β present, the same lesson one level up), #5094 and #5121 (the
transient-API-error recovery this gate disables), #5654.
Summary
A contributor relay recovers from a retryable API error by typing
try againinto theagent's pane. It correctly refuses to do that while a human is sitting at the pane β
nobody wants a watchdog typing over someone mid-keystroke. It decides whether a human
is there by asking tmux for
#{client_activity}.That field does not mean "a human typed." It means "this client sent us bytes," and
a terminal emulator sends bytes for reasons that have nothing to do with a person:
replies to the escape sequences the running application writes (capability queries,
colour queries, cursor-position reports), and β where enabled β mouse and focus events.
A Claude Code TUI issues such queries on its own schedule, so an attached but unused
terminal tab keeps
client_activityadvancing indefinitely.The consequence is that automatic recovery never runs for the most ordinary setup there
is.
just contribute-hive <backend> localprints an attach hint, contributors leave thesession attached in a terminal tab to watch it, and from then on every retryable API
error parks the task until a person notices and bumps it by hand. The relay logs its
reasoning each time, so the failure is at least visible β but it never resolves.
Blast radius: interactive mode, any backend, any contributor whose tmux session is
attached. Work is not lost, but a task stalls until a human intervenes, which is exactly
the unattended-operation property the retry logic exists to provide.
Observed, 2026-09-02
Contributor
Danathar, claude backend, working issue #5681. The agent hitβ API Error: Connection lost mid-response.β which classifies correctly, it is inTRANSIENT_API_ERROR_PATTERNSβ and then:Four consecutive ticks over six minutes, each recomputing a fresh "last input" age. The
contributor confirms they never typed into that pane β it is a background tab in their
terminal emulator. Working back from the log,
client_activityadvanced at 11:05:31,11:08:32 and 11:13:02: irregular gaps of ~3min and ~4.5min, which is not what human
typing looks like and not what a fixed poll looks like either.
Both of the obvious explanations are ruled out on this host:
No mouse reporting, no focus events β and
client_activitystill moves. What remains isthe terminal answering queries the application asked it.
HUMAN_PRESENCE_IDLE_MSis 5 minutes and the observed gaps are shorter than that, soactivenever went false and the 5-minute window never elapsed. Detaching the client(
tmux detach-client) is what released it.Cause
bin/contributor-relay.sh:and its consumer in
handleTransientAPIError:#5277 already made the right distinction once β an attached client is not a person, so
attachedalone must not block recovery. This is the same lesson one level down:client_activityis not a person either. The refinement that fixed #5277 replaced oneproxy with a slightly better proxy rather than with a measurement of what was actually
meant.
Worth noting the log line's wording,
last input Ns ago, states the assumption as fact.It is what made this hard to see: the message asserts a human typed, so the natural
reading is to doubt the contributor rather than the field.
Fix shape
There is no tmux field that means "a human pressed a key," so the honest options are:
pane is composing something, so the CLI's input area differs from empty between ticks.
The relay already captures the pane every tick for other purposes, so this costs
nothing new and is a direct observation rather than a proxy.
observed gaps are minutes apart, so a threshold above the largest plausible query
interval would clear it. This is tuning a proxy that is wrong in kind, and would
still misfire on a terminal that polls more often.
HIVE_ASSUME_UNATTENDED=1for contributorswho never intend to type. Escape hatch, not a fix, and it puts the burden on the
person least placed to know the mechanism.
and retry anyway: a task parked forever on a signal the relay cannot verify is worse
than one keystroke landing next to a human's. This is the smallest change that bounds
the damage, and composes with any of the above.
(1) plus (4) looks right: observe the thing that actually indicates composition, and
refuse to defer indefinitely on any presence signal.
Regression coverage: a fixture where
client_activityadvances on every tick with nochange to the pane's input line, pinned as not active β today's tests can only exercise
the proxy, which is why this passed.
Related: #5277 (attached β present, the same lesson one level up), #5094 and #5121 (the
transient-API-error recovery this gate disables), #5654.