Skip to content

fix(composer): treat an NBSP-padded composer as empty, not pending - #2980

Closed
wmbryce wants to merge 1 commit into
kunchenguid:mainfrom
wmbryce:fix/composer-nbsp-empty
Closed

wmbryce wants to merge 1 commit into
kunchenguid:mainfrom
wmbryce:fix/composer-nbsp-empty

Conversation

@wmbryce

@wmbryce wmbryce commented Aug 24, 2026

Copy link
Copy Markdown

The bug

fm_composer_classify_content classifies a composer holding a prompt glyph followed by U+00A0 (non-breaking space) as pending, identically to real typed text.

$ source bin/fm-composer-lib.sh
$ NBSP=$(printf '\xc2\xa0')
$ fm_composer_classify_content 1 '❯'            -> empty
$ fm_composer_classify_content 1 "❯${NBSP}"     -> pending      # the bug
$ fm_composer_classify_content 1 '❯ '           -> empty
$ fm_composer_classify_content 1 '❯ hello'      -> pending

The glyph-stripping case matches '❯ '* (glyph plus ASCII space), so an NBSP-padded row falls through to the single-character '❯'* branch and leaves \xC2\xA0 behind.
The following trim uses [![:space:]], and bash's [:space:] does not match U+00A0 in the C/POSIX locale, so the NBSP survives, the row reads non-empty, and the verdict is pending.

Why it matters

The away-mode daemon injects only on an affirmatively empty composer.
A Claude Code pane on the herdr backend renders its idle composer as ❯ followed by U+00A0, verified from a raw ANSI capture:

\e[38;2;153;153;153m❯\xC2\xA0

That pane can therefore never be injected into, so away-mode supervision is silently dead for it rather than degraded.

Observed on 2026-08-24: an overnight autonomous run stalled with 63,681 seconds (about 17.7 hours) of undelivered escalations.
Three workers completed and nothing after them started, because the wake path into the supervised session could not be used.
state/.supervise-daemon.log repeated:

inject deferred: supervisor composer not confirmed-empty (state=pending: pending input, dead-shell prompt, or unreadable pane)

The wedge alarm fired correctly and was the only signal that anything was wrong.

The fix

Treat Unicode whitespace as whitespace when judging composer emptiness, while keeping pending protective of real input.

A row of only invisible characters is empty.
Invisible characters around visible content remain pending, so a half-typed line is still protected:

fm_composer_classify_content 1 "❯${NBSP}"        -> empty
fm_composer_classify_content 1 "❯${NBSP}hello"   -> pending
fm_composer_classify_content 1 '❯ hello'         -> pending
fm_composer_classify_content 1 '❯'               -> empty

Also fixed: the deferral log conflated two verdicts

state=pending: pending input, dead-shell prompt, or unreadable pane named three conditions in one string.
Per the classifier's own contract, pending means real unsubmitted text, while a dead shell or an unreadable pane is unknown.
The conflated message sent the first reader looking for a dead shell that did not exist.
Each verdict now prints only what it actually means.

Tests

Extended tests/fm-composer-lib.test.sh rather than adding a runner.
Cases cover the bare glyph, glyph plus NBSP, glyph plus ASCII space, glyph plus NBSP plus real text, and a row of only invisible characters.
Tests exercise the public interface and assert no implementation bytes.

bin/fm-lint.sh passes.

…h unknown

The inject deferral line printed "pending input, dead-shell prompt, or
unreadable pane" for every non-empty verdict, so a `pending` composer sent
the first reader hunting a dead shell that did not exist. Each verdict now
prints only what it means, including the absent and unrecognized cases.

Also generalize the Unicode-whitespace boundary comment in the composer
library from U+200B alone to the whole zero-width format class (U+200B,
U+2060, U+FEFF), and state the two reasons the line sits there: mapping a
zero-width character onto a space would invent separation the harness never
drew, and widening `empty` is the unsafe direction while deferring is the
one the max-defer alarm surfaces.

Pin the boundary with regression coverage through the public classifier,
under the ambient locale and LC_ALL=C: unicode padding reads empty, padding
around real text stays pending, and zero-width chars defer.
@greptile-apps

greptile-apps Bot commented Aug 24, 2026

Copy link
Copy Markdown

Confidence Score: 5/5

The PR appears safe to merge; no concrete changed-code defect or vision conflict was identified.

The executable logging change covers the full composer verdict vocabulary and preserves safe deferral behavior, while the added tests and comments accurately pin existing locale-independent classification behavior.

Reviews (1): Last reviewed commit: "fix(supervise): stop the composer deferr..." | Re-trigger Greptile

@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: first look on current main 038d0f7ec6ba (#2942). Thank you, wmbryce.

class=corrective leftover. Issue #1988 was closed already-fixed on 8/23 via #2102 (fm_composer_normalize_trim_var + matrix test). Verified on THIS main, not from metadata:

fm_composer_classify_content 1 of glyph plus U+00A0 returns empty (ambient and LC_ALL=C). Glyph plus NBSP plus hello returns pending.

Main already maps U+00A0 (and the rest of Unicode White_Space outside ASCII) onto ASCII space before trim. tests/fm-composer-lib.test.sh test_matrix_claude_bare_nbsp_row already pins that idle Claude row as empty.

This DIFF vs main: bin/fm-composer-lib.sh is comments-only (zero-width White_Space=No rationale). Classifier logic is unchanged. Added tests re-pin existing behavior (FIGURE SPACE / NARROW NBSP / zero-width defer). bin/fm-supervise-daemon.sh splits the deferral log by verdict - a logging nit, not a remaining padding/path gap. Not a competing fix; closing rather than opening another PR.

VISION.md: closing aligns. Away-mode already injects on a genuinely empty NBSP-padded composer; authority/scripts/restart/spine/vendor/scope are n/a for a leftover close.

This HEAD: 6bef4f25e300b586119ce48fda4fb5943cdffc35. MERGEABLE / UNSTABLE, ahead 1 / behind 0. No no-mistakes-pipeline-attestation:v1. CI action_required 32780664193 / 32780664023 - not approved (closing). First-time fork; no security issue in the diff.

Land-eligible rec: NO (already on main via #2102). Captain-flag NOW: no.

Closing as already fixed. This is not waiting-on-author and not a captain-decision.

@kunchenguid

Copy link
Copy Markdown
Owner

Speaking as Kun's firstmate: closing as already fixed on main by #2102.

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.

2 participants