Skip to content

os/v3/puppy: the console-TUI frame is rebuilt in full once a second, and its log pane is not poll noise - #1093

Merged
bong-water-water-bong merged 3 commits into
mainfrom
os/v3-tui-logfilter
Nov 5, 2026
Merged

bong-water-water-bong merged 3 commits into
mainfrom
os/v3-tui-logfilter

Conversation

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator

Closes #1092. Cycle 26 item 1, the console half (~/briefs/TOS3-C26.md: tty1 came back garbled from the cycle-25 take -
"1EShow the API key", "engint: o ebit", duplicated MENU blocks).

Two defects in os/v3/puppy/console-tui, both measured on the take's own evidence, neither a substitute for the other.

1. The frame is rebuilt in full once a second

draw() runs ~4x/s, but ratatui writes only the cells whose content changed since its own last frame. That is correct
only while the screen still holds that frame, and nothing keeps other writers off tty1 on this appliance:

writer evidence
kernel printk the board's cmdline is console=ttyS0,115200 console=tty0 with no loglevel=, so kern.info and up are painted on tty1 - the c25 take's dmesg.log has usbcore: deregistering interface driver snd-usb-audio at t=1759 and an amdxdna TDR dump at t≈697
1bit-release-check npu_note() writes "\n 1bit check: is holding the NPU" to /dev/tty1
a handoff / anything else on this terminal 1bit-console-menu --action N runs a dialog program on tty1

Reproduced on the shipped c25 artifact (2fc6909c…, what the c25 image embeds): a real pty (tmux pane,
TERM=linux), one printf of three printk-shaped lines to the pane's own tty, then capture the pane:

size clean +0.15 s +1.0 s +2.2 s after a keypress
80x25 0 stray lines 2 2 2 2
240x67 0 4 4 4 -

Permanent, and a keypress does not heal it: the diff has nothing to notice. That is the owner's screenshot - and because
the stray write ends with a newline on the last row, the VT scrolls and the log pane's LEM lines are left painted over the
CONNECT rows and the MENU, which is what "engint: o ebit" looks like:

> 1 Show the API key                                                          8020
> 26-11-05 04:47:05.123 [Debug] (Server) GET /api/v1/models
> 3 Network (DHCP, static, hostname)                       2026-11-05 04:47:05.456
>  [Debug] (Server) POST /chat/completions

After (same pty, same single write, this branch's artifact): the stray text is gone at +1.0 s and stays gone, at
both 80x25 and 240x67.

Cost, measured by counting the bytes off a pty for 5 s (TERM=linux, idle dashboard):

size shipped 2fc6909c… this branch
80x25 0.4 KiB/s 1.7 KiB/s
240x67 (tty1) 1.0 KiB/s 4.5 KiB/s

Clear is one escape the repaint lands behind, so the VT shows no visible blank (a 240x67 frame is 4.5 KiB, i.e.
sub-frame on the console) - and a full frame per second is worth it: it bounds what any stray line can cost the person
standing at the box to one second.

2. The log pane drops the poll lines

The take's own log (/data/logs/lemond-c25-take.log, 2039 lines) is 549 × GET /api/v1/models - one every ~3 s from
the open demo pages - against 61 chats, 12 /internal/config, 6 image generations. The pane is three rows at 80x25 and
~40 at 240x67, so the polls were the pane, and every chat/load/error had scrolled out of it.

facts::poll_line drops the timer-driven GETs (/api/v1/models, /api/v1/health, /api/v1/stats). The method and the
exact path are both required, so a POST to the same path and /api/v1/models/<name>/options are kept. A tail that is
nothing but polls is shown unfiltered rather than blanked - an empty pane would print "no Lemonade log and no kernel
GPU/NPU lines yet" over a log that exists.

Measured on that same file, rendered at 240x67 (the take log staged at /var/log/lemond-onebit.log, then removed):

poll rows in the pane what the pane shows
shipped 2fc6909c… 29 GET /api/v1/models
this branch 0 POST /api/v1/images/generations - 200, the vae decode graph … 11.12s / generate_image completed in 15.75s lines, the POSTs

Tests

cargo test 53/53 (52 + the_log_pane_drops_the_polls_and_keeps_the_chats, which uses lines copied out of that log,
including two that only look like polls, and asserts the fallback for an all-poll tail). Removing the filter makes it red
(measured, then restored).

Also on the artifact this branch builds (0995cc95dffaa48c, 1,582,424 B, static, no PT_INTERP):

What this does NOT fix, on purpose

The writers. This is insurance for the console, not a licence to paint tty1: silencing printk (loglevel= on the
command line, mkimage.sh) and the two console notes (1bit-release-check, puppy-check.sh) are os/v3's files and are
being reported to pi-os3/pi-release with these measurements rather than edited from here. One measured fact worth their
time: the take's config.json has log_level: debug but no log_file, and Lemonade's log_file: auto means its
file sink is off (resolve_logging_targets: auto → targets.file = false), so the log_max_file_size_mb: 8 /
log_max_files: 3 keys in that file are inert and the only copy of lemond's log is 10lemonade's >> redirect.

Notes

  • Nothing else touches the console tree: git log 2fc6909c…origin/main -- os/v3/puppy/console-tui is empty between the
    c25 artifact and this base.
  • No image build, no QEMU, no board run, no device ticket, no GPU/NPU hold; :18097 and npu-serve-keeper untouched.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

For review, and one thing that is not this diff: the branch reads BEHIND because main moved to eb66dcd8
(10 files, tests/laya_*). git log bd7d451a..origin/main -- os/v3/puppy/console-tui is empty, so Update branch
is conflict-free and a no-op for the reviewed diff; I am not rebasing on my own tonight (each rebase restarts a ~6-minute
build and the fleet is merging every few minutes - this lane's strict_required_status_checks_policy means it has to be
current at merge anyway).

The artifact for the next cut is 0995cc95dffaa48c (1,582,424 B, static) if this merges; until then c25's
2fc6909c… stays correct. The two committed snapshots are byte-identical to the c25 baseline, so a c26 frame check
diffing against 2fc6909c… sees no movement at all from this PR.

@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

PR Reviewer Guide 🔍

(Review updated until commit 43861a7)

Here are some key observations to aid the review process:

🎫 Ticket compliance analysis 🔶

1092 - Partially compliant

Compliant requirements:

  • The console-TUI frame is now rebuilt in full at most once a second
  • The fix is measured on hardware and shows the cost is +3.5 KiB/s at 240x67
  • Stray lines written to tty1 are cleared within 1 second
  • The screen is healed without relying on keypresses

Non-compliant requirements:

  • None

Requires further human verification:

  • The exact hardware measurements and screen capture verification (as described in the issue) should be confirmed by a human reviewer

1014 - Partially compliant

Compliant requirements:

  • The MENU rule now gives way before an action does when there's insufficient space
  • The footer still shows "1-9 actions" even when the rule is not drawn
  • The layout at normal sizes is unchanged

Non-compliant requirements:

  • None

Requires further human verification:

  • None

1026 - Partially compliant

Compliant requirements:

  • The message now gives way before an action does when space is tight
  • The message is never cut mid-word
  • The message is drawn whole or not at all

Non-compliant requirements:

  • None

Requires further human verification:

  • None
⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Frame Rebuild Logic

The frame rebuild logic introduces a new last_heal variable to track when the screen was last cleared. While this addresses the issue of persistent console artifacts, the timing of the clear operation (once per second) may introduce a slight delay in screen recovery. However, this is acceptable given the hardware constraints and the measured performance impact.

let mut last_heal = Instant::now();
let rc = loop {
    while let Ok(f) = rx.try_recv() {
        let keep = app.log_scroll;
        app.facts = f;
        app.log_scroll = keep.min(app.facts.logs.len().saturating_sub(1));
    }
    if let Some((_, at)) = &app.message {
        if at.elapsed() > Duration::from_secs(12) {
            app.message = None;
        }
    }
    if !app.facts.reachable() && last_spin.elapsed() >= Duration::from_millis(250) {
        app.spin = app.spin.wrapping_add(1);
        last_spin = Instant::now();
    }
    // Nothing keeps other writers off tty1: kernel printk (the board's cmdline is `console=ttyS0,115200 console=tty0`,
    // so every amdgpu/amdxdna line is painted here), `1bit-release-check`'s NPU notice, a program the dialog menu ran.
    // ratatui writes only the cells whose CONTENT changed since its own last frame, so one of those lines is a wrong
    // screen that a keypress does not fix - measured: three stray lines and a keypress left every panel row showing
    // the log pane's text. So the frame is rebuilt in full, from a cleared screen, once a second: the write is a
    // Clear plus absolute positions (no full-screen flash on the VT) and the cost is one frame per second, which is
    // what a stray line can then cost the reader.
    if last_heal.elapsed() >= Duration::from_secs(1) {
        if term.clear().is_err() {
            break 1;
        }
        last_heal = Instant::now();
    }
Documentation Clarity

The README.md documentation provides good detail about the problem and solution, but could be slightly more precise in describing the exact conditions under which the full frame rebuild occurs. Specifically, it mentions that the frame is rebuilt "at most once a second" but doesn't clarify that this is a rate limit rather than a guaranteed frequency.

**A frame is rebuilt in full once a second, because a tty cannot be made private.** Anything holding this terminal open
can write to it, and ratatui writes only the cells whose *content* changed since its own last frame - correct only while
the screen still holds that frame. So one stray line leaves a wrong screen that **a keypress does not fix**: measured on
the shipped c25 artifact (`2fc6909c…`) in real ptys, three printk-shaped lines were still there at +2.2 s and after a
keypress, at 80x25 and at 240x67 - and when the write ends on the last row the VT scrolls, which is how the log pane's
own lines end up painted across the CONNECT block and the MENU (the cycle-25 screenshot: "`engint: o ebit`"). Who writes:
kernel messages while `console_loglevel` allows them, `1bit-release-check`'s "holding the NPU" line and
`puppy-check.sh`'s console note (both `/dev/tty1` on purpose), and any program a handoff runs. So the frame is rebuilt in
full, from a cleared screen, at most once a second: the same write is gone at **+1.0 s**, and the cost is measured -
`+3.5 KiB/s` at 240x67 (4.5 KiB/s, was 1.0).

**Measured on a booted appliance, not just a pty** (QEMU, the frozen c25 image, its own `tty1::respawn:/usr/local/bin/1bit-console`,
the guest's VGA text console at 160x50, screen read through `1bit-tty1` -> `/dev/vcsa1` the way the gates read it, plus QEMU
`screendump`s): with the shipped `2fc6909c…` two stray lines written to `/dev/tty1` were still on the screen 4.3 s later
**and after a keypress sent through QEMU's own keyboard**; with this binary the same write was gone by the next frame
(0 stray rows at +0.3 s, +1.5 s, +3.5 s and after a keypress) and the healed frame was byte-identical to the clean one
except the log pane's own new lines. What a screendump cannot catch is a sub-frame blank, so "the Clear is invisible"
remains arithmetic (one escape plus 4.5 KiB) - a person at the monitor or a 60 fps capture is the only judge of that.

This is insurance, not the fix for a noisy writer: a writer that paints this screen is still a defect in the writer.

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

CI on the head f532d0f5 (README-only commit on top of the reviewed diff): all green - build pass 6m34s,
Analyze ×5 pass, CodeQL pass, pr_agent pass 47s, hw_gate skipped (TUI-only change, no device ticket). 0 review
threads.

mergeStateStatus is BEHIND because main moved to 22c833cb; git log bd7d451a..origin/main -- os/v3/puppy/console-tui
is empty, so Update branch at merge time is conflict-free and a no-op for the reviewed diff. The committed snapshots
are byte-identical to the c25 baseline (2fc6909c…), so a c26 frame check sees no movement from this PR; the artifact if it
lands is 0995cc95dffaa48c (1,582,424 B, static - unchanged by the README commit, re-derived with build-static.sh).

…so a stray line cannot stick

The console's screen cannot be made private: anything holding the terminal open can write to tty1, and ratatui writes
only the cells whose CONTENT changed since its own last frame - correct only while the screen still holds that frame.
So one stray line leaves a wrong screen that a keypress does not fix, and when the write ends on the last row the VT
scrolls, which is how the log pane's own lines end up painted across the CONNECT block and the MENU (the cycle-25
screenshot: "engint: o ebit", duplicated MENU blocks).

Measured on the shipped c25 artifact 2fc6909c… in real ptys (TERM=linux, one printf of three printk-shaped lines to the
pane's tty, then capture the pane):

  size    clean  +0.15 s  +1.0 s  +2.2 s  after a keypress
  80x25   0      2        2       2       2
  240x67  0      4        4       4       -

Permanent. Now the frame is rebuilt in full, from a cleared screen, at most once a second: the same write is gone at
+1.0 s and stays gone, at both sizes. Cost measured by counting the bytes off a pty for 5 s (idle dashboard):
80x25 1.7 KiB/s (was 0.4), 240x67 4.5 KiB/s (was 1.0). The Clear is one escape and the 240x67 repaint is 4.5 KiB, so
the blank is well inside a display frame by arithmetic - NOT measured on the appliance's own VT, which a pty cannot
answer; the README says so.

Scope: src/main.rs and the README paragraph. The two other halves of cycle-26 item 1 - the log pane's poll noise and the
kernel's console level - are #1088's (they were drafted here first and are dropped from this branch in its favour, after
their measurements: console_loglevel is overruled to 4 by Ubuntu's sysctl.d, a fact this branch's draft had missed).

52 tests (unchanged - nothing here is unit-testable; the reproducer is in the PR body), clippy --all-targets clean,
fmt --check clean, tools/copyright.py --check rc 0, the two committed snapshots byte-identical, the eight gate strings
still on the 80x25 frame, the 78-run pty sweep identical to the shipped build's notes. Artifact da5f6763c74a15a5,
1,581,384 B, static.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Re-scoped, and why (also said on #1088, comment 6655549807): #1088 (pi-os3, opened 15 minutes before this one) is the
c26 item 1 PR and owns the pane filter and the kernel console level. I dropped both from this branch in its favour -
their filter labels how many lines it hid, and they measured a fact my draft had missed (Ubuntu's
/usr/lib/sysctl.d/55-console-messages.conf re-raises console_loglevel to 4 after rc.sysinit, so 1bit-console's own
dmesg -n was being overruled and the drop-in is the half that sticks). This PR is now src/main.rs + one README
paragraph (+28/−0)
and closes the repaint half of #1092, which is the half #1088 does not cover (their QEMU repro shows the
layout intact under polling - polling writes nothing to tty1; the permanence needs a writer, which is what the take's printk
and the two check hooks are).

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

CI on 9963252f (the re-scoped branch): build pass 5m28s, Analyze ×5 pass, CodeQL pass, hw_gate skipping (no
device code) — everything the ruleset requires is green. pr_agent is red again but its run reads
conclusion: cancelled in 1m51s (gh run view 42010099494) — the workflow concurrency group, not a review, and not a
required context; re-run requested. It reviewed the earlier 4-file head green in 47 s, so this is the same pattern.

mergeStateStatus=BEHIND (main moved); git log bd7d451a..origin/main -- os/v3/puppy/console-tui is empty, so Update
branch
is conflict-free and a no-op for the diff. Merge order with #1088 is free (test-merged, "Automatic merge went well").

…e's own VT, not only in a pty

The README said the one thing a pty could not answer was the appliance's own console. It has now been answered on a booted
frozen c25 image (QEMU, `tty1::respawn:/usr/local/bin/1bit-console`, guest VGA text console at 160x50): the screen read the
way the gates read it (1bit-tty1 -> /dev/vcsa1) plus QEMU screendumps of both states.

* shipped 2fc6909c...: two stray lines written to /dev/tty1 were still on the screen 4.3 s later AND after a keypress sent
  through QEMU's own keyboard (9 actions, 2 stray rows at +0.3/+3.3/+4.3 s) - the permanence is real on the VT, not just in
  a pty;
* this branch: the same write was gone by the next frame (0 stray rows at +0.3/+1.5/+3.5 s and after a keypress), and the
  healed frame is identical to the clean one except the log pane's own new lines (one row: the LOGS follow indicator).

Also recorded, because the second run of the harness found it: killing the TUI does NOT bring it back - 1bit-console
continues to exec 1bit-console-menu (its documented fallback), so tty1 becomes the dialog menu, which repaints on input and
therefore does not heal either (2 stray rows until a key is pressed). That is why the fix belongs in the ratatui screen.

What a screendump cannot catch is a sub-frame blank, so "the Clear is invisible" stays arithmetic (one escape + 4.5 KiB).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@bong-water-water-bong bong-water-water-bong left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VERDICT: READY at 3752ebcb — the mechanism is right for the reason given, and the crate's tests pass at this head.

The claim, checked against the code: the loop ticks at event::poll(Duration::from_millis(250)), so "draw() runs ~4x/s" is accurate, and the new heal is if last_heal.elapsed() >= Duration::from_secs(1) { term.clear() } before the draw. That is the correct instrument for this bug: ratatui diffs against its own previous buffer, so a stray line written by anything else leaves the screen wrong until a cell's content changes again — Terminal::clear() discards that buffer, so the next draw() repaints every cell. Without it, no amount of extra drawing fixes a screen someone else painted on.

cargo test --offline --release in os/v3/puppy/console-tui at this head: 52 passed, 0 failed, including the snapshot/size cases (the_mdns_name_is_on_screen_at_both_sizes, the_qr_is_drawn_beside_the_block_only_where_it_fits, the_menu_sits_between_the_facts_and_the_log). The error paths are handled the same way as the draw (break 1), so a terminal that cannot be cleared still exits the TUI rather than looping.

Two notes, neither blocking:

  1. The cost is a full repaint every second for as long as the TUI runs, which is the right trade for a screen nobody else may paint on — but it is also the reason the first half of item 1 (#1088: stop the writers) matters, and the two PRs should land together to be worth their cost.
  2. term.clear() clears the backend's screen; if it ever needs to be cheaper, the usual refinement is to clear only when a writer is known to exist. I would not do that now — a stray line you cannot see is worse than a repaint you cannot see.

I did not boot an appliance for this; the repaint is a Rust/VT mechanism and the crate tests plus the code are the evidence available off-device.

@bong-water-water-bong
bong-water-water-bong merged commit 3eebaa1 into main Nov 5, 2026
9 checks passed
@bong-water-water-bong
bong-water-water-bong deleted the os/v3-tui-logfilter branch November 5, 2026 11:31
@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown

Persistent review updated to latest commit 43861a7

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

Merged as 3eebaa1f — one record-keeping note and the artifact for the next cut.

The squash took the branch's tree exactly (git diff 3752ebcb 3eebaa1f -- os/v3/puppy/console-tui is empty; main
ad472533 builds da5f6763c74a15a5, 1,581,384 B, byte-identical to the branch head), and it does not carry the two
halves I dropped: git show main:.../src/facts.rs | grep -c poll_line = 0 and the 1bit-console printk block is absent -
those are #1088's.

But the squash title is the PR's first title ("…and its log pane is not poll noise"), while the merged diff is only the
repaint (src/main.rs + one README paragraph, +35/−0). Nothing to fix in the tree - just so the merge record is not read as
"the pane filter landed". #1088 (opened 15 min before this one) is still the PR that carries the pane filter and the kernel
console level, and its build is still red on one copyright notice (head dc9705f9; the fix is two lines, patch at
~/briefs/TTUI-PATCH-1088-copyright.diff, both ways spelled out in my comment 6656201051).

So c26 item 1 as it stands on main ad472533: the console heals (a stray line costs ≤1 s instead of the screen - measured
on a booted c25 image's own VT, table in the body above), and the pane is still poll noise + printk still paints tty1
until #1088 lands. If the cut happens before that, that is the honest state to describe.

bong-water-water-bong added a commit that referenced this pull request Nov 5, 2026
…tead of ending the console (#1102)

The module's first promise is "the console never goes dead ... a paint failure redraws", and the loop ended on the first paint
error - which is right for a terminal that cannot be written and wrong for `ErrorKind::Interrupted`, where a signal arrived
before the write reached the terminal, nothing was drawn, and the next pass should simply paint again. It is reachable:
crossterm's event source installs a SIGWINCH handler (that is how resize events arrive), so an ssh user dragging a window edge
is the realistic trigger - and #1093's once-a-second `term.clear()` added a second "one error ends the console" point.

`paint_failed()` is now the single classification: `true` for every kind except `Interrupted`. An interrupted clear is ignored
(the heal retries next pass); an interrupted draw skips the OSC-8 pass and repaints on the next iteration; anything else still
exits 1, which is what makes `1bit-console` show the dialog menu.

Measured: 53 tests (52 + `an_interrupted_paint_is_repainted_but_a_dead_terminal_ends_the_console`: Interrupted -> retry;
BrokenPipe/PermissionDenied/NotFound/Other -> fatal). 12 window resizes in 2 s while the TUI runs (tmux resize-window = a
SIGWINCH storm): session survives, nine actions, `q` still stays on the console. Heal unchanged on the happy path (stray write
gone at +1.0 s at 80x25), both committed snapshots byte-identical, the eight gate strings still on the 80x25 frame, clippy
--all-targets and cargo fmt --check clean, tools/copyright.py --check rc 0. Artifact 906705b04783a87d, 1,581,568 B, static.

Not measured: the EINTR itself (a pty cannot be made to interrupt crossterm's write on demand), so the classification is
unit-tested and the path reasoned - the resize storm and a fatal error's exit are the two ends that could be demonstrated.

Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
bong-water-water-bong pushed a commit that referenced this pull request Nov 5, 2026
… console heals)

c26 item 1 was "nothing may paint over the console", and #1093 made the console survive it - but nothing judged it afterwards, so
the first regression would be found by the owner's eyes again. This row asks the question the only way a gate can: write two
stray printk-shaped lines into the console the TUI owns, wait out one repaint, and require the frame's own landmarks again -
nine action rows, 'Reboot / power off', 'Show the API key', and zero 'stray line' rows.

The action count greps `^ ?[1-9] [A-Z]` on purpose: the MODELS panel prints `1 downloaded` once a model is downloaded, which a
plain `^ ?[1-9] ` counts as a tenth action (measured on a booted image).

It SKIPs rather than failing when the question does not apply: no `1bit-console-tui` on tty1 (the dialog-menu image - `dialog`
repaints on input by nature) or a `/dev/tty1` that is not writable.

Red/green measured in ONE boot of the frozen c25 image (harness: ~/wt/tui-vm/rowproof-console-heals.sh, report
~/wt/tui-vm/report-rowproof.txt):

  the SHIPPED console TUI (2fc6909c…):  actions=9 stray=2  ->  console-garbled   (RED)
  the merged artifact (da5f6763…):     actions=9 stray=0  ->  console-heals     (GREEN)

`bash -n` clean, tools/copyright.py --check rc 0, one file. Merge order with #1088 measured free in both directions ("Automatic
merge went well"): this row goes in above their `U2 console quiet`, and it needs #1093, which is already on main.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
bong-water-water-bong added a commit that referenced this pull request Nov 5, 2026
… console heals) (#1111)

* os/v3 puppy: a vm-check row for the console that holds its screen (U2 console heals)

c26 item 1 was "nothing may paint over the console", and #1093 made the console survive it - but nothing judged it afterwards, so
the first regression would be found by the owner's eyes again. This row asks the question the only way a gate can: write two
stray printk-shaped lines into the console the TUI owns, wait out one repaint, and require the frame's own landmarks again -
nine action rows, 'Reboot / power off', 'Show the API key', and zero 'stray line' rows.

The action count greps `^ ?[1-9] [A-Z]` on purpose: the MODELS panel prints `1 downloaded` once a model is downloaded, which a
plain `^ ?[1-9] ` counts as a tenth action (measured on a booted image).

It SKIPs rather than failing when the question does not apply: no `1bit-console-tui` on tty1 (the dialog-menu image - `dialog`
repaints on input by nature) or a `/dev/tty1` that is not writable.

Red/green measured in ONE boot of the frozen c25 image (harness: ~/wt/tui-vm/rowproof-console-heals.sh, report
~/wt/tui-vm/report-rowproof.txt):

  the SHIPPED console TUI (2fc6909c…):  actions=9 stray=2  ->  console-garbled   (RED)
  the merged artifact (da5f6763…):     actions=9 stray=0  ->  console-heals     (GREEN)

`bash -n` clean, tools/copyright.py --check rc 0, one file. Merge order with #1088 measured free in both directions ("Automatic
merge went well"): this row goes in above their `U2 console quiet`, and it needs #1093, which is already on main.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* os/v3 puppy: a skip marker is a SKIP, in one rule with a self-test (pi-prs' CHANGES on #1111)

`U2 console heals` reports why it cannot run with `console-heals-skip-no-tui` / `-no-tty1`, and the driver's verdict was
`want in body` — so both skip markers CONTAIN the row's own want string (`console-heals`) and a row that never ran printed
**PASS**. pi-prs caught it; this is the fix they named, done as the rule rather than as a rename of the markers (renaming
would leave the next author free to write `foo-skip-bar` for a row wanting `foo`).

* **One implementation**: `os/v3/puppy/vm_check_verdict.py` holds `skipping()` (a `*-skip-*` token, anchored on token
  boundaries so prose like "skips the check" cannot trigger it) and `verdict()`, which chooses the level in one place and
  in one order: **INFO** (nothing asserted) -> **SKIP** (the row could not run) -> **PASS/FAIL**. A skip is neither a pass
  nor a failure; it is a row this medium could not answer, and its reason travels into the table.
* **The driver uses it** (`VMCHECK_DIR` + `import vm_check_verdict as V`) instead of its own three-way if. It no longer
  formats either; the module returns the flattened line.
* **A self-test that costs no VM**, with the arms that would have caught the bug: both real skip markers -> SKIP (one of
  them for a want string that has nothing to do with the marker), a green body -> PASS, a garbled body -> FAIL, no want ->
  INFO, and a body whose *prose* mentions skipping -> PASS (the marker must be a token, not a word). `vm-check.sh
  --verdict-selftest` runs only these and exits; a full run calls them first and prints
  `PASS U0 verdict    the row-verdict rule holds: a *-skip-* marker is SKIP, not PASS`, failing the run with exit 2 if the
  arms ever stop holding.

Measured, on the row's real skip body: old rule `'console-heals' in 'console-heals-skip-no-tui'` -> **PASS**; new rule ->
**SKIP | console-heals-skip-no-tui**; `actions=9 stray=0 | console-heals` -> PASS; `... stray=2 | console-garbled` -> FAIL.

Checked before pushing, because today's lessons were about checks that cannot fail: `tools/copyright.py --check` rc=0 with
the tree staged (it only walks git-tracked files), the row list still parses (77 rows, `U2 console heals` present), and
`grep -- -skip-` over the whole file finds the markers only in this row, so no existing row changes verdict.

Row count: the `U0 verdict` line is a real row in the output, so a c26 medium now reads one line more (74 on this artifact);
said here because pi-release's expectations are keyed to the count.

---------

Co-authored-by: t <t@t>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: pi-os3 <pi-os3@1bit.monster>
bong-water-water-bong added a commit that referenced this pull request Nov 5, 2026
…nges and stops saying 52 tests (#1149)

The crate's README describes the frame heal (#1093) but not the two changes that arrived with
it, so a reader of the shipped crate cannot see why a control byte from Lemonade's log is
harmless, or why an interrupted paint does not end the screen:

  * #1118 - facts::screen_text() cleans text from outside (CSI and OSC removed whole, TAB to a
    space, other C0/DEL dropped) before it becomes a cell. Measured against the recorded take's
    own log: the shipped c26 candidate (5b21a0a8, heal and no sanitiser) leaves 4 escape bytes in
    one rendered frame and on a 240x67 pty cuts its progress row at the first ESC[K; this build is
    0 bytes. On the appliance's own 160x50 VT (booted c26 candidate, QEMU) the same wrapped
    progress line came back whole on BOTH binaries - the once-a-second full paint rewrites the
    cells the erase cleared - so the README calls this defence in depth rather than a visible
    repair, which is what the measurement says. Test:
    a_log_line_or_a_model_name_cannot_carry_an_escape_into_the_screen
  * #1102 - a paint that fails with ErrorKind::Interrupted repaints instead of ending the console
    (crossterm's SIGWINCH handler makes an ssh resize the realistic trigger); every other kind
    still exits to the dialog menu. Test:
    an_interrupted_paint_is_repainted_but_a_dead_terminal_ends_the_console

Also corrects the test count: cargo test is 55, not the 52 the README has said since #1050/#1059.

Docs only - no code, no frame, no gate string touched. cargo test 55/55, tools/copyright.py --check rc 0.

Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
bong-water-water-bong added a commit that referenced this pull request Nov 6, 2026
…ree rows that could skip themselves (#1218)

c29 ships /usr/local/bin/1bit-selftest (engine #1166) - the one-command proof the install sheet asks the
operator to run - and no row ever executed it. The file was proved present and byte-identical to
os/mini/selftest.sh, and proved by nobody that it runs in the appliance: the same class the U4 release-check
row was added for in cycle 21.

The new row does not demand 'RESULT: PASS'. The image boots with an empty /data/models, where the tool's own
documented answer is 'RESULT: FAIL - no model to test', so demanding PASS would red a healthy image - and a
row that reds a healthy image gets deleted. It requires a VERDICT: PASS with a model, SKIP naming the
no-model reason without one, FAIL when the binary is absent or dies without a RESULT line.

Its first version printed selftest-broken-missing and came back SKIP, and that is how the second half of this
commit was found: Puppy's shell echoes the typed command into the row's own output, vm_check_verdict.skipping()
matches a *-skip-* token anywhere in the body, and the command carried the marker literally - so the row was
skipped by the echo of itself. Two rows already in the tree carry a literal marker and are exposed to the same
thing. U1 shipped shells (#1186, merged tonight) was measured self-skipping, i.e. the row that exists to catch
an unparsable script had never run; U2 console heals (#1093) is latent. All three now assemble the marker from
m/sk, so the token exists only in the output. No row's semantics change.

Proved: four arms under bash and sh on fixtures (absent / no-model / pass / dies silently); the module's own
verdict mapping on the echoed command before and after (SKIP with token m-skip-no-model -> no token, so the
output decides); and end to end on two shipped artifacts, c28 0c371992 (no such file -> FAIL
selftest-broken-missing, and U1 now RUNS: 'shells-parse: 45 scripts') and c29 8266aff9 (SKIP
selftest-skip-no-model on its blank models dir, 77 PASS / 0 FAIL / 4 SKIP).

Co-authored-by: pi-os3 <os3@1bit.invalid>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

os/v3/puppy: a console-TUI frame is never repainted, so one line written to tty1 is a wrong screen for ever

1 participant