fix(composer): treat U+00A0 as whitespace so an idle Claude composer reads empty #4
Loading…
Reference in a new issue
No description provided.
Delete branch "fm/composer-nbsp-fix"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Fixes the away-mode wedge that cost two full nights: 5h50m on 2026-08-05 and 7h32m on 2026-08-07.
The symptom
The captain goes to bed with work running. Workers finish within about fifteen minutes. The captain is never told and firstmate is never woken, so a fleet that could have kept working idles until morning. On return,
bin/fm-afk-return.shreportsaway-mode inject WEDGED: 27181s undelivered.state/.supervise-daemon.logcarries 3,362 occurrences, one every ~15 seconds continuously since the 5th, of exactly one line:The cause: one invisible character
Claude renders its idle composer row as the prompt glyph
❯(U+276F) followed immediately by U+00A0 NO-BREAK SPACE — not an ASCII space.fm_composer_classify_contenttrims trailing whitespace with the bash idiom${var%"${var##*[![:space:]]}"}, and bash's[:space:]does not include U+00A0 (verified under bothLC_ALL=Canden_US.UTF-8). So once the glyph is stripped, the NBSP survives as "content", is non-empty, matches no idle placeholder, and the verdict ispending.The daemon's guard then does exactly what it should: refuses to inject into a composer it believes holds unsent text. Forever.
The guard's logic was never wrong. Its idea of what a real idle composer looks like was.
Why the existing tests did not catch it
tests/fm-afk-inject-e2e.test.shpasses both its scenarios — partial input defers injection and swallowed Enter produces exactly one clean digest. Its fixtures use an ASCII space. The injection path was covered, the coverage was green, and the defect lived in the gap between the fixture and the real terminal.The fix
Normalize U+00A0 to a regular space at the top of
fm_composer_classify_content— one shared leaf, so every backend and caller benefits. Eight lines, with a comment recording the reasoning so it is not "simplified away" later.What it lets through, stated explicitly
Verified against the real classifier:
emptypendingpendingemptyThe anti-corruption guarantee is not weakened. A NBSP between the glyph and real input collapses to a space, and the typed text still reads
pending, so the daemon still refuses to inject over anything the captain has started typing.Cleared of suspicion
a73484c(locale-independent glyph strip) is not implicated — the defect reproduces before and after it.⟩glyph is not implicated. This branch is rebased onto the merged upstream and both changes coexist: the NBSP normalization and all six⟩occurrences are present.Residual, deliberately not fixed here
The fix targets U+00A0 because that is the observed byte. Other Unicode whitespace bash also excludes — U+2009 thin space, U+2003 em space — would behave identically if a harness ever emitted one. None is observed in live captures. A general "trim all Unicode whitespace" helper would be more robust but is a much larger change; this is the smallest fix that resolves the observed defect.
The second half, already done
The wedge had a masking condition: when the daemon gave up and raised its max-defer alarm, the alarm had nowhere to go.
config/wedge-alarmwas absent, absent meansauto, andautoresolves to a macOS-only channel — this fleet is Linux. The only output was a marker file nothing reads. A workingherdrchannel is now configured and verified firing. That is local config, not part of this change.Tests
fm-composer-lib10/10,fm-composer-ghost31/31,fm-backend-herdr189/189 — all green on the rebased branch, plus two new regression tests pinning the glyph+NBSP shape.