Conversation
…poser Claude draws its borderless composer between two horizontal `─` rules. When the pane is focused, herdr overlays the terminal title on the TOP rule, so that rule renders as dashes PLUS text and fails the dashes-only separator predicate. The pair therefore never opens, and the composer's own BOTTOM rule becomes an unmatched separator sitting below the `❯` row the scan had already matched. The separated-shape staleness rule reads an unmatched separator below a candidate as proof the candidate is scrollback, discards the correct match, and returns `unknown` - which defers away-mode injection indefinitely. Only a focused pane carries the title overlay, so supervisor panes failed while worker panes stayed readable. Narrow the consumer rather than loosen the predicate: the dashes-only separator test also feeds the pi identity conjunction, where being wrong about what closes a separated composer would promote an unidentified blank region into an injection target. It is left strictly as-is. Instead, the staleness invalidation now spares a bare agent glyph that is immediately sandwiched between a rule above (solid or titled) and the window's only separator directly below - Claude's composer box. Adjacency on both edges preserves the staleness rule everywhere else, because a glyph genuinely stranded in scrollback has transcript rows, not its own box edges, between it and the separator below. This is the herdr counterpart of the cmux borderless-composer fix (kunchenguid#2029). Verification: - Portable regression covers the four-way titled/clean rule by NBSP/ASCII-space glyph matrix and asserts the divergence, so the case cannot go vacuously green; reverting only the consumer narrowing reproduces the exact failure. - Live guard exercises the real focused herdr pane, which no tmux fixture can render, and reports explicitly when no focused pane was available. - Negative cases confirm unchanged verdicts: a dead shell row and a blank row still read `unknown`, and typed text still reads `pending`.
Contributor
Author
|
Closing this in favour of a replacement raised through the required no-mistakes submission path. Root cause of the stuck check on this PR: no-mistakes had no fork URL recorded for this repo, so it could not push and the PR was raised by hand, which is why the submission-signature check never passed. That configuration is now fixed. The replacement also widens the scope deliberately: the titled-rule fix here refuses a title containing a non-ASCII character, and the real failing title on the affected host began with one, so this fix would not have covered the case it was written for. The replacement corrects the width proof for non-ASCII titles instead, and refuses strictly more malformed input than this version did. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A focused Claude pane's composer classified
unknowninstead ofemptyunder the herdr backend, which indefinitely defers away-mode escalation injection: the daemon readsunknownas "cannot prove this composer is safe to type into" and correctly declines. The composer was in fact idle and empty.This is the herdr counterpart of the cmux borderless-composer fix (#2029). Same underlying shape, different backend surface, and it survived that fix because it fails through a different rule.
Root cause
Claude draws its borderless composer between two horizontal
─rules. While a pane is focused, herdr overlays the pane's terminal title on the top rule, so that rule renders as dashes plus text:The dashes-only separator predicate correctly rejects that titled row, so the separated-shape pair never opens. The composer's own bottom rule is then an unmatched separator sitting below the
❯row the scan had already matched correctly. The separated-shape staleness rule reads an unmatched separator below a candidate as proof the candidate is stale scrollback, discards the correct match, and returnsunknown.Only a focused pane carries the title overlay, which is why supervisor panes failed while worker panes stayed readable — the asymmetry that makes this easy to miss.
The fix: narrow the consumer, not the predicate
The dashes-only separator predicate also feeds the Pi identity conjunction, where being wrong about what closes a separated composer would promote an unidentified blank region into an injection target. That is a genuine safety gate, so it is left strictly as-is.
Instead the staleness invalidation now spares a bare agent glyph that is immediately sandwiched between a rule above (solid or titled) and the window's only separator directly below — Claude's composer box. Adjacency on both edges is what preserves the staleness rule everywhere else: a glyph genuinely stranded in scrollback has transcript rows, not its own box edges, between it and the separator below.
_fm_composer_rule_rowis anchored at both ends and requires every non-dash residue to be ASCII-printable or whitespace, so a row carrying box-drawing structure of its own can never pass as a plain rule.Why two distinct defects had to be fixed for this to work
This shape needs the glyph row read as empty and the composer row kept from being discarded. Either one alone still yields a wrong verdict:
❯+ U+00A0 NBSPunknownempty❯+ ASCII spaceunknownempty❯+ U+00A0 NBSPemptyempty❯+ ASCII spaceemptyemptyClaude renders its idle prompt as
❯followed by U+00A0, not an ASCII space. The NBSP half of this pair — accepting a glyph row whose only content is a non-ASCII whitespace character — was fixed for all backends by #2102 via the sharedFM_COMPOSER_UNICODE_SPACESnormalisation, which is why the bottom two rows already pass onmain. That normalisation is character-safe rather than byte-slicing, so it holds underLC_ALL=Ctoo (the hazard behind #883).This PR fixes the remaining titled-rule half. The two are independent: on
mainthe titled cases fail regardless of which whitespace character the glyph row carries, because the row is discarded before its content is ever judged.Verification
Live probe, before and after on identical bytes. The real focused pane was captured once (2504 bytes of ANSI) and classified with both versions of the library, so no pane state change between runs can explain the difference:
Live guard (
FM_COMPOSER_MATRIX_LIVE=1 tests/fm-composer-matrix-live-e2e.test.sh), herdr 0.8.0 + claude 2.1.226.634:The
focused=trueline is the exact shape that failed. Because the title overlay exists only on a focused pane, no tmux fixture can render it — this is the harness-dependent half of the required test pair, and the guard emits an explicit note rather than passing silently when no focused Claude pane was available to exercise it.Portable regression covers all four matrix cells with no harness present and asserts the divergence deliberately, so the case cannot go vacuously green. Reverting only the consumer narrowing reproduces the exact failure (
expected empty, got 'unknown').Negative cases confirm no widening of
empty. Wrongly reading a composer as empty is worse than the original bug, since it would let the daemon type into a shell prompt or over half-typed input:$ > % #on a titled row ->unknownunknown(strict blank-row posture preserved)pendingunknownempty, working ->unknown, identity probe absent ->unknownThe strict separator predicate that gates Pi identity still rejects a titled rule, so recognizing the rule did not relax that gate.
Testing
tests/fm-composer-lib.test.sh— 29 ok, 0 failurestests/fm-composer-ghost.test.sh— 33 ok, 0 failurestests/fm-backend-herdr.test.sh— 171 ok, 0 failurestests/fm-crew-state.test.sh— 49 ok, 0 failuresbin/fm-test-run.sh --changed— 57 scripts selected, 0 failuresbin/fm-lint.sh— clean (pinned ShellCheck 0.11.0)bin/fm-doc-audience-check.sh— clean (surfaces=67 local_links=236)Dated per-harness evidence recorded in
docs/verification/runtime-backends.mdunder a new "Herdr focused-pane titled composer rule" section, pointing at the live guard as the refresh command.