A placeholder hint in an empty input box reads as the operator's draft, so the box gate holds every delivery forever #782

Open
opened 2026-10-05 19:39:20 +02:00 by ltms · 3 comments
Owner

Found by the operator, who said plainly: "nothing in its input box!!!!! its empty, something wrong" and then named the cause: "there are input hint inside the input box -> it is the confusion?" Yes. Measured against the deployed jar (fleetd/run/fleetd.jar), with controls in both directions:

typed text         -> Reading[state=DRAFT, characters=18]
hint in empty box  -> Reading[state=DRAFT, characters=31]      <- the defect
truly empty        -> Reading[state=EMPTY, characters=0]
empty + spaces     -> Reading[state=EMPTY, characters=0]

Probe: PromptBox.classify called directly on four synthetic panes, run as a single-file program in package dev.ltms.fleet.herdr against the live jar. The two EMPTY rows are negative controls — without them a broken probe returning DRAFT for everything would look like a finding.

Mechanism

PromptBox.boxContent takes the box line, drops the marker, then keeps every character that is not whitespace and not in CURSOR_GLYPHS. Anything left makes the reading DRAFT:

return content.isEmpty() ? new Reading(State.EMPTY, 0) : new Reading(State.DRAFT, content.length());

So the detector cannot tell the operator's typing from text the TUI drew. The class javadoc already names this exact case as the accepted risk:

counts as the operator's unsubmitted text, including a placeholder hint a future TUI might draw there; that direction holds a delivery it could have sent, which HOLD_WARN_STREAK makes visible

That reasoning was sound when a hint was hypothetical. It is now the live TUI's behaviour, and the chosen failure direction has a worse consequence than the one it was avoiding.

Why "hold rather than clobber" is the wrong trade once the hint is permanent

A clobber is one damaged line, visible immediately, and recoverable. A permanent hold silently kills the whole fleet channel for that pane, with one WARN after 20 holds and nothing after that. Observed on this host: a pane held deliveries 25+ consecutive times across two separate windows, while two peers' messages sat undelivered for over 20 minutes. The sender was told accepted and pending — worker working the entire time (#780).

It is also undetectable from either side. The receiver sees an empty box and concludes the channel is broken; the sender sees success. Both peers on this host drifted onto Claude Code's SendMessage, which has no box gate, without anyone deciding to.

HOLD_WARN_STREAK does not rescue this: it makes the hold visible in the daemon log, which neither the sender nor the receiver reads.

Proposed

  1. Bound the hold. After a streak, stop holding. Either deliver and accept the clobber risk, or fail the message loudly so the sender learns. An unbounded hold must not be reachable. This is the one fix that works whatever the TUI draws next.
  2. Do not infer "the operator is typing" from box content alone. Content cannot carry that information. If herdr can expose styling, a dim hint is distinguishable from typed text; if it can expose cursor position, a cursor at column 0 of an otherwise non-empty line is a hint, not a draft. Either is a real signal where character counting is not.
  3. Surface the hold where the humans are. The receiver should be able to see that messages are queued for it (#780 requirement 4 asks for queue depth in fleet_list's panes row). A receiver that can see "3 queued, held: prompt box" fixes this in seconds.
  4. A hint catalogue is a last resort. It is brittle and it will rot silently — the same failure as keying on a proxy that the upstream owner is free to change.

Related

  • #780 — the sender is told pending — worker working for exactly these held messages, and a successful pane delivery logs nothing at all.
  • #781 — the other half of the same exchange: a reply that loses a 13-second race is rejected with an error naming the broker.
  • The BOX_MARKERS list is ["❯", "│ >"]; a previous ticket established that ❯ is the branch that fires on the live TUI and │ > matches nothing here. So marker detection is working; only the content classification is at fault.

Not measured

The live hold at the time of writing was caused by real typed text, not a hint: the pane showed ❯ cleared the input box and the daemon read DRAFT (19 characters) against my hand count of 18 non-whitespace. I cannot account for that one extra character, and it may indicate a glyph missing from CURSOR_GLYPHS — worth checking, because if a bare cursor were counted then an empty box would never read EMPTY, and the control above shows it does. An earlier reading of DRAFT (38 characters) on the same pane is consistent with a hint but I cannot prove it, because the pane content changed before I read it.

Found by the operator, who said plainly: *"nothing in its input box!!!!! its empty, something wrong"* and then named the cause: *"there are input hint inside the input box -> it is the confusion?"* Yes. Measured against the deployed jar (`fleetd/run/fleetd.jar`), with controls in both directions: ``` typed text -> Reading[state=DRAFT, characters=18] hint in empty box -> Reading[state=DRAFT, characters=31] <- the defect truly empty -> Reading[state=EMPTY, characters=0] empty + spaces -> Reading[state=EMPTY, characters=0] ``` Probe: `PromptBox.classify` called directly on four synthetic panes, run as a single-file program in package `dev.ltms.fleet.herdr` against the live jar. The two EMPTY rows are negative controls — without them a broken probe returning DRAFT for everything would look like a finding. ## Mechanism `PromptBox.boxContent` takes the box line, drops the marker, then keeps **every character that is not whitespace and not in `CURSOR_GLYPHS`**. Anything left makes the reading `DRAFT`: ```java return content.isEmpty() ? new Reading(State.EMPTY, 0) : new Reading(State.DRAFT, content.length()); ``` So the detector cannot tell the operator's typing from text the TUI drew. The class javadoc already names this exact case as the accepted risk: > counts as the operator's unsubmitted text, including a placeholder hint a future TUI might draw there; that direction holds a delivery it could have sent, which `HOLD_WARN_STREAK` makes visible That reasoning was sound when a hint was hypothetical. It is now the live TUI's behaviour, and the chosen failure direction has a worse consequence than the one it was avoiding. ## Why "hold rather than clobber" is the wrong trade once the hint is permanent A clobber is one damaged line, visible immediately, and recoverable. **A permanent hold silently kills the whole fleet channel for that pane**, with one WARN after 20 holds and nothing after that. Observed on this host: a pane held deliveries 25+ consecutive times across two separate windows, while two peers' messages sat undelivered for over 20 minutes. The sender was told `accepted` and `pending — worker working` the entire time (#780). It is also undetectable from either side. The receiver sees an empty box and concludes the channel is broken; the sender sees success. Both peers on this host drifted onto Claude Code's `SendMessage`, which has no box gate, without anyone deciding to. `HOLD_WARN_STREAK` does not rescue this: it makes the hold visible **in the daemon log**, which neither the sender nor the receiver reads. ## Proposed 1. **Bound the hold.** After a streak, stop holding. Either deliver and accept the clobber risk, or fail the message loudly so the sender learns. An unbounded hold must not be reachable. This is the one fix that works whatever the TUI draws next. 2. **Do not infer "the operator is typing" from box content alone.** Content cannot carry that information. If herdr can expose styling, a dim hint is distinguishable from typed text; if it can expose cursor position, a cursor at column 0 of an otherwise non-empty line is a hint, not a draft. Either is a real signal where character counting is not. 3. **Surface the hold where the humans are.** The receiver should be able to see that messages are queued for it (#780 requirement 4 asks for queue depth in `fleet_list`'s `panes` row). A receiver that can see "3 queued, held: prompt box" fixes this in seconds. 4. A hint catalogue is a last resort. It is brittle and it will rot silently — the same failure as keying on a proxy that the upstream owner is free to change. ## Related - #780 — the sender is told `pending — worker working` for exactly these held messages, and a successful pane delivery logs nothing at all. - #781 — the other half of the same exchange: a reply that loses a 13-second race is rejected with an error naming the broker. - The `BOX_MARKERS` list is `["❯", "│ >"]`; a previous ticket established that `❯` is the branch that fires on the live TUI and `│ >` matches nothing here. So marker detection is working; only the content classification is at fault. ## Not measured The live hold at the time of writing was caused by **real typed text**, not a hint: the pane showed `❯ cleared the input box` and the daemon read `DRAFT (19 characters)` against my hand count of 18 non-whitespace. I cannot account for that one extra character, and it may indicate a glyph missing from `CURSOR_GLYPHS` — worth checking, because if a bare cursor were counted then an empty box would never read EMPTY, and the control above shows it does. An earlier reading of `DRAFT (38 characters)` on the same pane is consistent with a hint but I cannot prove it, because the pane content changed before I read it.
Author
Owner

Correction: the hint is a real mechanism, but the live box line carries a non-breaking space, and that is the measured defect

I chased the one unexplained character from the issue body (daemon DRAFT (19) against my hand count of 18) and it is not a rounding slip. It is U+00A0, a no-break space, and boxContent counts it as the operator's text.

Measured by dumping the live box line's codepoints. Both idle lead panes on this host, read with herdr pane read … --lines 40 and classified by calling PromptBox.classify on the capture against the deployed jar:

wB:p1 -> Reading[state=DRAFT, characters=19]
  U+00A0 ' '        <- counted as text
  U+0063 'c' U+006C 'l' U+0065 'e' U+0061 'a' U+0072 'r' U+0065 'e' U+0064 'd'
  U+0020 (whitespace)
  …
wA:p1 -> Reading[state=DRAFT, characters=16]
  U+00A0 ' '        <- counted as text
  U+0067 'g' U+006F 'o'
  U+0020 (whitespace)
  …

So the TUI draws U+00A0 between the ❯ marker and the text, while the gaps inside the typed text are ordinary U+0020. The classifier skips the U+0020 and keeps the U+00A0, which is the whole of the one-character discrepancy.

The cause is a Java character-class detail, not a logic error:

isWhitespace(U+00A0) = false
isSpaceChar(U+00A0)  = true

Character.isWhitespace excludes the no-break spaces (U+00A0, U+2007, U+202F) by specification. boxContent's filter is Character.isWhitespace(c) || CURSOR_GLYPHS.indexOf(c) >= 0, so every no-break space reaches the content buffer.

What that costs, with controls

empty box, ASCII space padding -> Reading[state=EMPTY, characters=0]     (control)
empty box, U+00A0 padding      -> Reading[state=DRAFT, characters=38]
empty box, one U+00A0          -> Reading[state=DRAFT, characters=1]
typed text, U+00A0 lead-in     -> Reading[state=DRAFT, characters=6]     ("hello" is 5)

The ASCII row is the negative control: the filter does work, for the space the TUI does not use.

Two consequences, and they are not equally proven:

  1. Measured: every DRAFT count this gate reports is one too high on this TUI, because of the lead-in. Small, but it is a number in a log line that a human reads while diagnosing a dead channel, so it should be right.
  2. Latent and serious: if the TUI ever draws that padding across an empty box, State.EMPTY becomes unreachable and the pane never receives anything again. The synthetic row above shows the classifier's half of that. I have not measured the TUI's half, because no pane here currently has an empty box — both leads hold typed text.

Evidence that the TUI does not pad an empty box today: gated deliveries have succeeded on this host (task-368d62-7 reached wB:p1), and a padded empty box would have made that impossible. That is an inference from a successful delivery, not a direct reading of an empty box, and it is the thing to measure before sizing this.

Does this change the fix?

It sharpens proposal 2 and adds a cheap one:

  • Treat a no-break space as whitespace. Character.isWhitespace(c) || Character.isSpaceChar(c) covers U+00A0, U+2007 and U+202F in one clause and needs no list to maintain. This is correct on its own terms — a no-break space is padding whatever draws it — so it is worth doing even though it is not today's outage.
  • Proposal 1 (bound the hold) is unchanged and still the one that holds whatever the TUI does next. This finding is an argument for it: the gate's input is a styled text region owned by someone else, and we just found a second way for it to read as text that no one typed.

What is not the current cause

The holds observed on this host right now are real typed text: wB:p1 holds cleared the input box and wA:p1 holds a line starting go . The hint mechanism in the issue body is real and reproducible, but no hint is on screen. wB:p1 also shows ← 1 agent, a live subagent, which makes it WORKING — a second, independent gate that an empty box would not clear.

## Correction: the hint is a real mechanism, but the live box line carries a non-breaking space, and that is the measured defect I chased the one unexplained character from the issue body (daemon `DRAFT (19)` against my hand count of 18) and it is not a rounding slip. It is **U+00A0, a no-break space**, and `boxContent` counts it as the operator's text. Measured by dumping the live box line's codepoints. Both idle lead panes on this host, read with `herdr pane read … --lines 40` and classified by calling `PromptBox.classify` on the capture against the deployed jar: ``` wB:p1 -> Reading[state=DRAFT, characters=19] U+00A0 ' ' <- counted as text U+0063 'c' U+006C 'l' U+0065 'e' U+0061 'a' U+0072 'r' U+0065 'e' U+0064 'd' U+0020 (whitespace) … wA:p1 -> Reading[state=DRAFT, characters=16] U+00A0 ' ' <- counted as text U+0067 'g' U+006F 'o' U+0020 (whitespace) … ``` So the TUI draws **U+00A0** between the `❯` marker and the text, while the gaps *inside* the typed text are ordinary U+0020. The classifier skips the U+0020 and keeps the U+00A0, which is the whole of the one-character discrepancy. The cause is a Java character-class detail, not a logic error: ``` isWhitespace(U+00A0) = false isSpaceChar(U+00A0) = true ``` `Character.isWhitespace` excludes the no-break spaces (U+00A0, U+2007, U+202F) by specification. `boxContent`'s filter is `Character.isWhitespace(c) || CURSOR_GLYPHS.indexOf(c) >= 0`, so every no-break space reaches the content buffer. ### What that costs, with controls ``` empty box, ASCII space padding -> Reading[state=EMPTY, characters=0] (control) empty box, U+00A0 padding -> Reading[state=DRAFT, characters=38] empty box, one U+00A0 -> Reading[state=DRAFT, characters=1] typed text, U+00A0 lead-in -> Reading[state=DRAFT, characters=6] ("hello" is 5) ``` The ASCII row is the negative control: the filter does work, for the space the TUI does not use. Two consequences, and they are not equally proven: 1. **Measured: every `DRAFT` count this gate reports is one too high** on this TUI, because of the lead-in. Small, but it is a number in a log line that a human reads while diagnosing a dead channel, so it should be right. 2. **Latent and serious: if the TUI ever draws that padding across an empty box, `State.EMPTY` becomes unreachable and the pane never receives anything again.** The synthetic row above shows the classifier's half of that. I have **not** measured the TUI's half, because no pane here currently has an empty box — both leads hold typed text. Evidence that the TUI does *not* pad an empty box today: gated deliveries have succeeded on this host (`task-368d62-7` reached `wB:p1`), and a padded empty box would have made that impossible. That is an inference from a successful delivery, not a direct reading of an empty box, and it is the thing to measure before sizing this. ### Does this change the fix? It sharpens proposal 2 and adds a cheap one: - **Treat a no-break space as whitespace.** `Character.isWhitespace(c) || Character.isSpaceChar(c)` covers U+00A0, U+2007 and U+202F in one clause and needs no list to maintain. This is correct on its own terms — a no-break space is padding whatever draws it — so it is worth doing even though it is not today's outage. - Proposal 1 (**bound the hold**) is unchanged and still the one that holds whatever the TUI does next. This finding is an argument for it: the gate's input is a styled text region owned by someone else, and we just found a second way for it to read as text that no one typed. ### What is *not* the current cause The holds observed on this host right now are **real typed text**: `wB:p1` holds `cleared the input box` and `wA:p1` holds a line starting `go `. The hint mechanism in the issue body is real and reproducible, but no hint is on screen. `wB:p1` also shows `← 1 agent`, a live subagent, which makes it `WORKING` — a second, independent gate that an empty box would not clear.
Author
Owner

This is the real cause, and the signal to fix it exists. The gate reads the one source that throws it away.

This comment is newer than the brief and it wins. The operator said three times that the input boxes were empty. I twice read the glyphs, saw words, and reported them as the operator's typing. That was wrong both times. The boxes are empty. The words are Claude Code's placeholder hint, and it is drawn dim.

The measurement

herdr pane read takes --format {text,ansi}, --ansi and --raw, which the box gate does not use. With the escape codes kept, the box line is:

'❯\xa0\x1b[0m\x1b[2mcleared the input box\x1b[0m'
   SGR: ['0', '2', '0']

\x1b[2m is SGR 2, faint. The whole of the text sits inside it.

Across every pane on this host, read with --source visible --ansi, taking the last box line of each:

w1_p17     dim=False SGR=[]              text=''
w1_p1G     dim=False SGR=[]              text=''
w1_pY      dim=False SGR=[]              text=''
w2_p3M     dim=False SGR=[]              text=''
wA_p1      dim=True  SGR=['0', '2', '0'] text='go ahead with both'
wB_p1      dim=True  SGR=['0', '2', '0'] text='cleared the input box'

Every box line that carries text is dim. The five empty boxes carry no styling and no text, which is the negative control: an empty box with no hint is not being mis-styled into looking empty.

What I do not have is a positive control. No pane here currently holds real typed text, so I have not measured that typed text comes through without SGR 2. Do not ship the dim test without building that case — type a character into a pane and read it. If typed text were also dim, this test would classify a real draft as empty and clobber it, which is the failure this gate exists to prevent.

Why the gate cannot see any of this

PromptBox.PROBE_SOURCE is "detection", and that source strips every escape code. Byte counts from the same pane, same --lines 40, same moment:

--source visible            --ansi   3178 bytes, 16 lines containing ESC
--source recent             --ansi   3178 bytes, 16 lines containing ESC
--source recent-unwrapped   --ansi   3178 bytes, 16 lines containing ESC
--source detection          --ansi   2610 bytes,  0 lines containing ESC
--raw                                3178 bytes, 16 lines containing ESC

So on detection the hint and a real draft are byte-identical apart from their words. No amount of care inside classify can separate them, because the distinguishing information is removed before classify is called. The class javadoc predicted this exact case and accepted it; the accepted risk has now happened.

What this changes in your brief

Defect 1 (no-break space) stands as written — fix it.

Defect 2 (bound the hold) stands as written, and is still the fix that holds whatever the TUI does next. Build it.

Add defect 3, and treat it as the main one. classify must stop calling a dim box line a draft:

  1. Read a source that keeps the styling. AgentControl.read(target, source) takes the source name; check whether it can ask for ANSI at all, and if it cannot, that plumbing is part of this fix. If making that reachable needs a change outside PromptBox.java and PromptBoxTest.java, stop and tell me in your reply rather than editing a third file — two other workers are in this repo and I will take that change myself.
  2. Treat a box line whose visible content is entirely inside SGR 2 as EMPTY.
  3. Keep the no-break space handling from defect 1. The hint is preceded by U+00A0 as well, so both defects are present on the same line.
  4. Build the positive control named above before trusting the dim test, and if you cannot produce real typed text to measure, say so in your reply and leave the dim test behind the bounded hold rather than in front of it.

One more reason not to lean on the dim test alone: it is a property of one TUI's styling, so it is a proxy, and the program that draws it is free to change it. The bounded hold is the part that does not depend on anyone else's rendering choices. Ship both, and make the bounded hold the thing that guarantees the channel recovers.

## This is the real cause, and the signal to fix it exists. The gate reads the one source that throws it away. **This comment is newer than the brief and it wins.** The operator said three times that the input boxes were empty. I twice read the glyphs, saw words, and reported them as the operator's typing. That was wrong both times. The boxes are empty. The words are Claude Code's placeholder hint, and it is drawn **dim**. ### The measurement `herdr pane read` takes `--format {text,ansi}`, `--ansi` and `--raw`, which the box gate does not use. With the escape codes kept, the box line is: ``` '❯\xa0\x1b[0m\x1b[2mcleared the input box\x1b[0m' SGR: ['0', '2', '0'] ``` `\x1b[2m` is SGR 2, faint. The whole of the text sits inside it. Across every pane on this host, read with `--source visible --ansi`, taking the last box line of each: ``` w1_p17 dim=False SGR=[] text='' w1_p1G dim=False SGR=[] text='' w1_pY dim=False SGR=[] text='' w2_p3M dim=False SGR=[] text='' wA_p1 dim=True SGR=['0', '2', '0'] text='go ahead with both' wB_p1 dim=True SGR=['0', '2', '0'] text='cleared the input box' ``` Every box line that carries text is dim. The five empty boxes carry no styling and no text, which is the negative control: an empty box with no hint is not being mis-styled into looking empty. **What I do not have is a positive control.** No pane here currently holds real typed text, so I have not measured that typed text comes through *without* SGR 2. Do not ship the dim test without building that case — type a character into a pane and read it. If typed text were also dim, this test would classify a real draft as empty and clobber it, which is the failure this gate exists to prevent. ### Why the gate cannot see any of this `PromptBox.PROBE_SOURCE` is `"detection"`, and that source strips every escape code. Byte counts from the same pane, same `--lines 40`, same moment: ``` --source visible --ansi 3178 bytes, 16 lines containing ESC --source recent --ansi 3178 bytes, 16 lines containing ESC --source recent-unwrapped --ansi 3178 bytes, 16 lines containing ESC --source detection --ansi 2610 bytes, 0 lines containing ESC --raw 3178 bytes, 16 lines containing ESC ``` So on `detection` the hint and a real draft are byte-identical apart from their words. No amount of care inside `classify` can separate them, because the distinguishing information is removed before `classify` is called. The class javadoc predicted this exact case and accepted it; the accepted risk has now happened. ### What this changes in your brief Defect 1 (no-break space) stands as written — fix it. Defect 2 (bound the hold) stands as written, and is still the fix that holds whatever the TUI does next. Build it. **Add defect 3, and treat it as the main one.** `classify` must stop calling a dim box line a draft: 1. Read a source that keeps the styling. `AgentControl.read(target, source)` takes the source name; check whether it can ask for ANSI at all, and if it cannot, that plumbing is part of this fix. **If making that reachable needs a change outside `PromptBox.java` and `PromptBoxTest.java`, stop and tell me in your reply rather than editing a third file** — two other workers are in this repo and I will take that change myself. 2. Treat a box line whose visible content is entirely inside SGR 2 as `EMPTY`. 3. Keep the no-break space handling from defect 1. The hint is preceded by U+00A0 as well, so both defects are present on the same line. 4. Build the positive control named above before trusting the dim test, and if you cannot produce real typed text to measure, say so in your reply and leave the dim test behind the bounded hold rather than in front of it. One more reason not to lean on the dim test alone: it is a property of one TUI's styling, so it is a proxy, and the program that draws it is free to change it. The bounded hold is the part that does not depend on anyone else's rendering choices. Ship both, and make the bounded hold the thing that guarantees the channel recovers.
Author
Owner

Correction to my own comment: the no-break space does NOT make EMPTY unreachable. The hint is the only blocker.

The trinotes pane started working between my two readings, its hint vanished, and that gave me the measurement I said I did not have. I had written that the no-break space could make State.EMPTY unreachable. On the source the gate actually reads, it does not. I had only built that case synthetically and I should not have put it as high as I did.

The same pane, empty box, no hint, read both ways:

--source visible    box line repr: '❯\xa0'
--source detection  box line repr: '❯'
                    codepoints after marker: []

Then PromptBox.classify on the detection capture:

live pane -> Reading[state=EMPTY, characters=0]

So herdr's detection source trims the trailing no-break space when nothing follows it. The gate sees a bare marker and correctly reads the box as empty. That is why gated deliveries have been landing on this host all along — the inference I drew from task-368d62-7 was right, and now it is measured rather than inferred.

What the no-break space still is. It appears only when something follows it, so it is a +1 on every DRAFT count the gate logs, and nothing worse today. Worth fixing — a human reads that number while diagnosing a dead channel — but it is not the outage, and it must not be presented as one.

Revised standing of the three defects:

What it is Severity now
1 U+00A0 counted as text cosmetic: every DRAFT count is one too high
2 unbounded hold the structural fix; still build it
3 dim hint read as a draft the outage

The hint appears only while the pane is idle

The anki lead pointed out that the hint text in its own box was the exact sentence the operator had typed to it earlier, so the "hint" looked like its own last submitted prompt rather than fixed placeholder text. The trinotes transition supports that and adds the trigger.

Two readings of wA:p1, minutes apart, nothing else changed by me:

idle     dim=True  SGR=['0','2','0']  text='go ahead with both'
working  box line: '❯\xa0'            text=''          pane shows "✻ Zesting…" and "← 3 agents"

So the hint is drawn when the pane is idle and disappears when a turn starts. Taken with the anki observation, the hint is the pane's own last submitted prompt, shown dim while idle.

Treat that as a property of one TUI version, not a law. Both readings come from the same Claude Code build, so they are one instrument and not two independent confirmations.

It does explain the earlier varying counts (38, 31, 27 reported at different times) with no extra theory: the hint is whatever that pane last submitted, so its length changes with the conversation. No separate padding mechanism is needed to account for them.

It also narrows the blast radius. A pane only misses deliveries while it is idle and has submitted something before — which is exactly the state a pane sits in when it is waiting for a message. So the gate fails precisely when it is most needed, and recovers on its own the moment the pane gets busy, which is when delivery is correctly refused anyway. That is the worst possible pairing and it is why this looked intermittent.

Nothing changes in what to build

Defect 3 is still the fix, defect 2 is still the guarantee, defect 1 is still worth the one clause. The positive control I asked for in my previous comment is still missing and still required: I have not measured that real typed text arrives without SGR 2. Until someone types a character into a pane and reads it back, the dim test must sit behind the bounded hold, not in front of it.

## Correction to my own comment: the no-break space does NOT make EMPTY unreachable. The hint is the only blocker. The trinotes pane started working between my two readings, its hint vanished, and that gave me the measurement I said I did not have. I had written that the no-break space could make `State.EMPTY` unreachable. On the source the gate actually reads, it does not. I had only built that case synthetically and I should not have put it as high as I did. **The same pane, empty box, no hint, read both ways:** ``` --source visible box line repr: '❯\xa0' --source detection box line repr: '❯' codepoints after marker: [] ``` Then `PromptBox.classify` on the detection capture: ``` live pane -> Reading[state=EMPTY, characters=0] ``` So herdr's `detection` source trims the trailing no-break space when nothing follows it. The gate sees a bare marker and correctly reads the box as empty. That is why gated deliveries have been landing on this host all along — the inference I drew from `task-368d62-7` was right, and now it is measured rather than inferred. **What the no-break space still is.** It appears only when something follows it, so it is a `+1` on every `DRAFT` count the gate logs, and nothing worse today. Worth fixing — a human reads that number while diagnosing a dead channel — but it is **not** the outage, and it must not be presented as one. Revised standing of the three defects: | | What it is | Severity now | |---|---|---| | 1 | U+00A0 counted as text | cosmetic: every `DRAFT` count is one too high | | 2 | unbounded hold | the structural fix; still build it | | 3 | dim hint read as a draft | **the outage** | ### The hint appears only while the pane is idle The anki lead pointed out that the hint text in its own box was the exact sentence the operator had typed to it earlier, so the "hint" looked like its own last submitted prompt rather than fixed placeholder text. The trinotes transition supports that and adds the trigger. Two readings of `wA:p1`, minutes apart, nothing else changed by me: ``` idle dim=True SGR=['0','2','0'] text='go ahead with both' working box line: '❯\xa0' text='' pane shows "✻ Zesting…" and "← 3 agents" ``` So the hint is drawn when the pane is idle and disappears when a turn starts. Taken with the anki observation, the hint is the pane's own last submitted prompt, shown dim while idle. Treat that as a property of one TUI version, not a law. Both readings come from the same Claude Code build, so they are one instrument and not two independent confirmations. **It does explain the earlier varying counts** (38, 31, 27 reported at different times) with no extra theory: the hint is whatever that pane last submitted, so its length changes with the conversation. No separate padding mechanism is needed to account for them. **It also narrows the blast radius.** A pane only misses deliveries while it is idle *and* has submitted something before — which is exactly the state a pane sits in when it is waiting for a message. So the gate fails precisely when it is most needed, and recovers on its own the moment the pane gets busy, which is when delivery is correctly refused anyway. That is the worst possible pairing and it is why this looked intermittent. ### Nothing changes in what to build Defect 3 is still the fix, defect 2 is still the guarantee, defect 1 is still worth the one clause. The positive control I asked for in my previous comment is still missing and still required: I have **not** measured that real typed text arrives without SGR 2. Until someone types a character into a pane and reads it back, the dim test must sit behind the bounded hold, not in front of it.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: fleet/fleetd#782