Skip to content

os/v3/puppy: cycle 18 — the chat page answers with thinking off and a model picker, the first headless screen says how to connect, and the desktop hands the screen back to the menu - #925

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

Conversation

@bong-water-water-bong

@bong-water-water-bong bong-water-water-bong commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

os/v3/puppy: cycle 18 — the chat page answers with thinking off and a model picker, the first headless screen says how to connect, and the desktop hands the screen back to the menu

Cycle 18's orders are the c17 board demo (~/briefs/TOS3-C18-items.md) plus the owner's mid-cycle item 4 and the
one image-side finding in RELEASE-C17-VERDICT.md. Four fixes, each measured on the artifact or in a browser,
none of them a lowered gate.

1. A thinking model spent the answer budget thinking (c18-1)

Qwen3 on the c17 stick put its whole reply in reasoning_content; the page read only delta.content, so the
visible answer stopped mid-sentence and the follow-up printed done: 0 chunks in 5.9 s. The page now asks for
enable_thinking: false by default (Lemonade's own extension — bin/resources/docs/api/openai.md:55; the
chat_template_kwargs spelling in the brief is the MCP planner's knob, not this one), keeps a "show thinking"
switch that sends true and renders the reasoning dimmed above the answer, raises max_tokens 512 → 1024,
and names the two shapes that used to look identical: reasoning and no answer, and the stream ended without
[DONE]
.

2. No model picker (c18-2)

A <select> built from /api/v1/models, filtered to downloaded (a catalogue entry that is not on the box would
turn a demo into a 1-4 GB download), labelled GPU / NPU from the model's own recipe_options.onebit_backend
(with a -NPU-1bit id as the fallback). ?model= still pins one; an id that is not on the machine is ignored.

3. The first headless screen says how to connect (c18-4, owner 14:1x)

The console menu's prompt now carries a CONNECT block above the menu — hostname + http://<ip>:13305, the
OpenAI-compatible base http://<ip>:13305/api/v1 with the key pointed at menu item 1 (never printed on a screen
a recording catches), ssh root@<ip>, and Lemonade Mobile's server URL — and while the wire or Lemonade is still
coming up it names what it is waiting for with a spinner and redraws every 2 s instead of 15. No address yet is
its own line pointing at menu 3.

Fitted to the console by measurement: 80×25 leaves 12 prompt lines in a 24-row dialog with nine menu rows, so the
screen text is 9 lines (header, 4 connect, 3 status, 1 hint; every line ≤ 78 columns) and the status block is
compacted for the screen only — --status still prints every field, which is what the rows and the board read.

4. Menu item 6 left the screen on X's VT (from the c17 verdict)

1bit-desktop starts X on VT3; when the session ended the console stayed there showing xwin's "Exited from X"
text while the menu was alive on VT1. It now runs chvt 1 (busybox's applet is in this image) and says so in the
boot's log.

Proven, in a browser and on a pty

~/wt/os3i/c18-page-probe.py — a server that speaks the shapes the payload's own docs describe, plus a real Pale
Moon under Xvfb:

default            {"model":"Qwen3-0.6B-NPU-1bit","max_tokens":1024,"enable_thinking":false,"stream":true}
"show thinking" on enable_thinking:true -> [thinking] Okay, so the user … (dimmed) then the answer
                   status: done: 5 answer chunks, 8 thinking chunks (dimmed above), 0.7 s (stop)
reasoning only     status: done: 0 answer chunks, 8 thinking chunks (dimmed above), 0.4 s (length)
                           - no answer text: the model spent its budget thinking, untick 'show thinking'
picker             qwen3-0.6b-q4_k_m-1bit · GPU   /   Qwen3-0.6B-NPU-1bit · NPU
?model=<undownloaded id> is ignored

The probe caught a bug of mine that reading the diff would not have: ask() refilled the model list before every
prompt and reset the choice, so the header said one model and the request carried another. Fixed and re-measured.

The menu, in a pty at exactly 80×25 (/tmp/c18-menu-pty.py, two redraws with the spinner moving): all nine items
drawn with no scroll indicator, the CONNECT block and all four of its lines on screen; with no address, BOOT waiting for network … - plug the cable in, or menu 3. My first cut (4 connect + 7 status + a wrapping hint) left the
dialog scrolling at 88 % with item 9 "Reboot / power off" undrawn — the cycle-15 failure, caught here.

The artifact, and the gates on those bytes

Cycle 18 is ~/wt/puppy-out-c18b/, built from this branch at 3a428849, payload
lemonade-onebit-rc-2026-11-03-c21 (engine 0b04696e…), libhsa f6ef689d…, kernel
7.3.0-rc4-1bit-perfopt+ (out-c16). pi-release has verified the sha256 from their own read of the file.

file size sha256
1bit-os-puppy.img (ESP 512 MB + 1BIT-DATA 8 GiB) 9,128,902,656 B 22c60ec4f506d1eacad970f87614bbb9d13e5d61e247221ef5505e3f5a956bd1
upup64r-26.04.iso (side artifact) 1,347,420,160 B 884dceec0a985bcd65d114c98b2d8cc54e3c32d1fdb1c20acc0de3446350073f
gate result
build.sh resolute64 rc=0, all 13 of its own ok: checks (packed-initrd guard, packed-SFS uid/gid scan, console menu executable, ADDLIST=ec_su_axb35)
mkimage.sh rc=0, four grub entries, kernel signature asserted
the four changes are in the packed artifact unsquashfs -cat on the puppy SFS: menu carries CONNECT/screen_text/spinner, page carries enable_thinking/reasoning_content/MAXTOK = 1024, desktop carries the chvt 1 return
g3-probe.sh --cert ~/wt/mok-kernel.pem --require-mok rc=0 — /vmlinuz verifies against the enrolled MOK, 3780 modules, amdxdna (from updates/) + ec_su_axb35 signer and vermagic, firmware present
g2-probe.sh rc=0 twice (the SFS layered on our kernel)
prepare-stick.sh on a copy ten assertions PASS, key b076cd…, id_c15test key in authorized_keys
vm-check.sh on the prepared copy, Secure Boot on 47 PASS / 0 FAIL / 2 SKIP, twice — ~/wt/os3i/g18-vm2.log (unpatched harness) and ~/wt/os3i/g18-vm3.log (this head). SKIPs are the two board rows. Copies are booted, never the hand-off file

Cycle-18 rows from that run:

PASS U2 tty1          1bit OS - 1bit-os - lemonade-onebit-rc-2026-11-03-c21 | … | tty1-no-wizard
PASS U2 menu connect  1bit OS … | CONNECT 1bit-os web UI http://10.0.2.15:13305
                      | api http://10.0.2.15:13305/api/v1 (key: menu item 1)
                      | ssh root@10.0.2.15 (your key in /data/ssh/authorized_keys)
                      | app Lemonade Mobile: server http://10.0.2.15:13305 + that key | menu-connect-block
PASS U3 webapp page   thinking=4 reasoning=4 picker=1 max_tokens=1 | webapp-thinking-and-picker
PASS U3 inbound       -P INPUT DROP | -A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
                      | -A INPUT -p tcp -m tcp --dport 13305 -j ACCEPT | … | inbound-open
PASS U3 lan           http=200 on the forwarded :13305 (host :21305) with the image's key
PASS U7 ssh           a completed login, ssh-shell=/bin/bash, ssh-bash

One harness defect found by the first gate run, and fixed here

The first vm-check run on this image reported FAIL U3 lan http=000 and FAIL U7 ssh connection refused while
every in-guest row passed, sshd listening on 0.0.0.0:22 and hosts.allow carrying the LAN line included. The
two forwarded rows run after the row driver, the qemu budget was 300 s, and cycle 18's two new rows pushed the
boot past it, so the forwards were gone. The harness now runs 600 s and a host-side row that cannot reach a forward
asks whether qemu is alive and says "the VM was no longer running when this row ran (qemu budget Ns) — not an image
finding"
. Re-run unchanged: green. Nothing was lowered: U3 inbound was added, so the firewall/listener/
hosts.allow mechanism behind those two rows is measured instead of assumed.

Not claimed

  • c18-3 (the NPU stop / TDR) is not settled here. The re-check needs the NPU model on the board (this box has no
    /data/models/Qwen3-0.6B and both devices are held by other lanes); the TDR itself is an engine issue
    (~/briefs/NPU-MULTITURN-TDR-2026-11-03.md). What cycle 18 gives the next board demo is the answer on screen.
  • No mDNS: avahi-daemon is not in this image (measured in cycle 15), so the block prints the hostname and the
    address, not 1bit-os.local. The same block belongs in pi-tui's TUI header — another lane's file
    (~/briefs/TCONSOLE-TUI.md).
  • The page is still single-turn (it sends one message); the c18 item-3 correction notes the follow-up is where the
    board's TDR appeared, so adding history is a separate decision, not a silent change.

pi-os3 added 3 commits November 3, 2026 13:48
…er, and the desktop hands the screen back to the menu (cycle 18)

The c17 board demo showed two things about the conversation and one about the screen, three causes, all
measured on the artifact or in the browser:

1. A thinking model (Qwen3) puts its whole reply in reasoning_content, so a 512-token budget could be spent
   before a single delta.content arrived: the page showed a mid-sentence stop and then 'done: 0 chunks in
   5.9 s'. The page now sends enable_thinking:false by default (Lemonade's own extension, documented at
   bin/resources/docs/api/openai.md), keeps a 'show thinking' switch that renders reasoning_content dimmed
   above the answer, gives the answer 1024 tokens, and names the two shapes that used to look identical: a
   stream that produced reasoning and no answer, and one that ended without [DONE].

2. The model could only be chosen with ?model= in the URL. There is a dropdown now, built from
   /api/v1/models, filtered to what is downloaded and labelled GPU/NPU from the model's own
   recipe_options; ?model= still pins one and an id that is not on the machine is ignored.

3. 1bit-desktop starts X on VT3 and left the console there when the session ended, so the menu was alive on
   VT1 and the screen showed xwin's 'Exited from X' text. It now runs chvt 1 (busybox's chvt is in the
   image) and says so in the boot's log.

Proven in a browser, not by reading the file: Pale Moon under Xvfb against a server that speaks the payload's
documented shapes (~/wt/os3i/c18-page-probe.py) - default request carries enable_thinking:false and
max_tokens 1024; the switch makes it true and the reasoning draws dimmed; a reasoning-only answer reads
'done: 0 answer chunks, 8 thinking chunks (dimmed above) ... no answer text: the model spent its budget
thinking'; picking the second model puts it in the request and in the header (the probe caught a real bug
there: ask() refilled the list and threw the pick away). New vm-check row 'U3 webapp page' reads the served
page off the running proxy.
…shes every 2 s while it is still booting (cycle 18 item 4)

The owner: 'when you first boot the headless there is no data to display on how to connect, just the menu'. The
console menu's prompt now carries a CONNECT block above the menu - hostname and web UI http://<ip>:13305, the
OpenAI-compatible base http://<ip>:13305/api/v1 with the key pointed at menu item 1 (never printed here: this is
the surface a recording catches), ssh root@<ip>, and Lemonade Mobile's server URL - and while the wire or
Lemonade is still coming up it says what it is waiting for with a spinner and the box redraws every 2 s instead
of 15. No address yet is its own line pointing at menu 3.

Fitted to the console, not assumed: 80x25 leaves 12 prompt lines in a 24-row dialog with nine menu rows, so the
screen text is 9 (header, 4 connect lines, 3 status lines, 1 hint, each <= 78 columns) and the status block is
compacted for the screen only - --status still prints every field, because the rows and the board read that.
Measured in a pty at exactly 80x25 (two redraws, spinner moving): all nine items drawn with no scroll indicator,
CONNECT and all four of its lines present; the first cut of this (4 connect + 7 status + a wrapping hint) left
the box scrolling at 88% with item 9 'Reboot / power off' undrawn - the c15 failure, caught before it shipped.

New vm-check row U2 menu connect reads the block off tty1's own VGA buffer (the screen, not the script) and
requires the CONNECT line plus a real address in the web UI URL.
…e image, and opens the budget for the rows cycle 18 added

The cycle-18 run reported 'FAIL U3 lan http=000' and 'FAIL U7 ssh connection refused' while every in-guest row
passed, including sshd listening on 0.0.0.0:22 and hosts.allow carrying the LAN line. The cause was the harness:
the two forwarded rows run AFTER the row driver, the qemu budget is 300 s, and cycle 18's two new rows (the tty1
CONNECT block and the served page) pushed the boot past it - so the forwards were gone and 'connection refused'
read as an image defect. Re-run unchanged: 47 PASS / 0 FAIL / 2 SKIP (board rows), U3 lan http=200 and a completed
U7 login.

Two durable fixes: the budget is 600 s, and a host-side row that cannot reach a forward now asks whether qemu is
still running and says 'the VM was no longer running when this row ran (qemu budget Ns) - not an image finding'.

Also new: U3 inbound prints the mechanism the two forwarded rows depend on and had lost - iptables -S INPUT (the
-P INPUT DROP plus the two ACCEPT rules zz-onboard-ports adds after rc.firewall), the :22/:13305 listeners and the
1bit-lan line in /etc/hosts.allow - because 'nothing answered' cannot otherwise be told apart from 'the appliance
never opened the port'. Measured on the c18b image: both dport rules live, both listeners up, hosts.allow line
present.

And the U2 menu connect row matches the screen's spelling ('api http://', not 'api  http://'): dialog collapses runs
of spaces in a --menu prompt, so the passing row printed two misleading missing-* words.
@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

PR Reviewer Guide 🔍

(Review updated until commit 9c89eae)

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

VM Timeout Extension

The VM timeout has been extended from 300s to 600s, which may mask underlying performance issues or inefficiencies in the VM boot or test execution process. This change should be validated to ensure it doesn't hide real problems or lead to longer test cycles without justification.

secs=${3:-600}
Model Picker Implementation

The model picker implementation introduces a new dropdown for selecting models, but the logic for handling model selection and ensuring the correct model is used in requests needs careful review to prevent issues with model switching or incorrect model usage in chat completions.

function fill() {
  return fetch(api + "/api/v1/models?show_all=true", { headers: headers() })
    .then(function (r) { return r.json(); })
    .then(function (j) {
      var all = (j && j.data) || [];
      have = all.filter(function (m) { return m.downloaded !== false; });
      // a 1bit model first: this page is the appliance's chat, and the appliance's models are ours
      have.sort(function (a, b) { return (/1bit/i.test(b.id) ? 1 : 0) - (/1bit/i.test(a.id) ? 1 : 0); });
      while (sel.options.length) { sel.remove(0); }
      if (!have.length) {
        var o = document.createElement("option");
        o.textContent = "no model on this machine - console menu item 4, Models";
        o.disabled = true; sel.appendChild(o); sel.disabled = true;
        throw new Error("the server lists no downloaded model - pull one from the console menu (item 4)");
      }
      have.forEach(function (m) {
        var o = document.createElement("option");
        o.value = m.id; o.textContent = label(m);
        sel.appendChild(o);
      });
      sel.disabled = false;
      // a pick already made survives a refill - ask() refills before every prompt, and resetting the model
      // there silently threw the dropdown's choice away (measured in the cycle-18 browser probe: the header
      // said qwen3-0.6b-q4_k_m-1bit and the request carried the NPU model)
      if (want && have.some(function (m) { return m.id === want; })) model = want;
      else if (!model || !have.some(function (m) { return m.id === model; })) model = have[0].id;
      sel.value = model;
      show();
      return model;
    });
}
Connect Block Display

The introduction of the CONNECT block in the console menu requires careful validation to ensure it displays correctly and provides accurate information about network connectivity, API access, and SSH details. The logic for determining when to show the CONNECT block versus the booting text needs to be verified.

screen_text() {
    header_line
    if net_up && lemonade_up; then connect_text; else booting_text; fi
    status_brief
    menu_text
}
VT Switching Logic

The logic for switching the console back to VT1 after X exits needs to be verified for compatibility across different systems and ensure it correctly handles cases where chvt is not available.

# Cycle 18 (pi-release's c17 finding): X runs on VT3, and when it exits the console STAYS there, showing
# xwin's "Exited from X" text while the menu is alive on VT1 - so menu item 6's promise ("the menu comes back
# when X exits") was true in the process table and false on the screen. Put the screen back on the menu's VT.
if command -v chvt > /dev/null 2>&1; then
    chvt 1 2> /dev/null && say "== 1bit-desktop: console back on VT1 (the menu)"
elif busybox chvt 1 2> /dev/null; then
    say "== 1bit-desktop: console back on VT1 (the menu, busybox chvt)"
else
    say "== 1bit-desktop: no chvt here: the console stays on X's VT, Ctrl+Alt+F1 returns to the menu"
fi

@bong-water-water-bong

Copy link
Copy Markdown
Collaborator Author

#925 ready for review at b447ed8 — cycle 18: chat page answers with enable_thinking:false + a GPU/NPU model picker, the first headless screen carries a CONNECT block (2 s refresh while booting), and 1bit-desktop gives the screen back to the menu. Evidence: 47 PASS / 0 FAIL / 2 SKIP vm-check rows under Secure Boot on the c18b image, G2/G3 green, and the page/menu proven in a real browser and a 80x25 pty.

@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: CHANGES at b447ed8d3 — one measured gate defect. Everything else in the diff that I could check host-side reads right.

The defect: two of the five CONNECT checks in this PR's own U2 menu connect row no longer match anything, and the row still passes.

The menu emits the block with two spaces (1bit-console-menu:220-223, and #921's TUI deliberately matches it — "in the dialog menu's own wording", both surfaces keep api http:// / ssh root@ because the cycle-18 row was written against them). This PR changes the row's word list to single-spaced:

vm-check.sh:466  "for w in CONNECT 'web UI http://' 'api http://' 'ssh root@' 'Lemonade Mobile'; do "

Against the block the menu actually prints (taken from this head's screen_text()):

$ s='CONNECT  strix   web UI http://10.0.2.15:13305
   api  http://10.0.2.15:13305/api/v1   (key: menu item 1)
   ssh  root@10.0.2.15   (your key in /data/ssh/authorized_keys)
   app  Lemonade Mobile: server http://10.0.2.15:13305 + that key'
$ for w in CONNECT 'web UI http://' 'api http://' 'ssh root@' 'Lemonade Mobile'; do printf '%s' "$s" | grep -qF "$w" || echo missing-$w; done
missing-api http://
missing-ssh root@
$ printf '%s' "$s" | grep -qE 'web UI http://[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+:13305' && echo menu-connect-block
menu-connect-block

So the two missing- lines print, and menu-connect-block prints anyway — the row's expected token is present, so the run reads PASS while api and ssh were never asserted. With the two-space words (what the branch had before, and what the text contains) all five match, which is why the VM run showed its PASS output intact: the disarming is silent by construction.

Two fixes, both small, and I would take both:

  1. restore the two-space words ('api http://', 'ssh root@') — or normalise: s=$(printf '%s' "$s" | tr -s ' ') before the greps;
  2. make a missing word fail the row: miss= instead of echo missing-…, then [ -z "$miss" ] && echo menu-connect-block. As written, the diagnostic exists but nothing consumes it, which is the same shape as the zero-exit traps in #657.

This matters past this PR because #921 (console TUI) is READY and renders the same block with the same two spaces — after this merges, the row would check neither surface for api/ssh.

What I did check and found sound:

  • the CONNECT block itself: the four lines are on screen (I diffed the emitted strings above), CONNECT carries the hostname, api points at menu item 1 rather than printing the key, and the no-address case is its own line pointing at menu 3.
  • the desktop handback: 1bit-desktop:100-106 tries chvt 1, falls back to busybox chvt 1, and if neither exists says so and names the Ctrl+Alt+F1 route — no silent path.
  • the qemu-budget guard: secs=${3:-600}, vm_alive() on the qemu command line, vm_gone() wording "not an image finding", and the two forwarded rows now ask before they blame the image. That is the right shape for the c17 false FAIL.
  • the page: enable_thinking:false by default with the "show thinking" switch, max_tokens 1024, the picker filtered to downloaded with a GPU/NPU label, and the state fix for ask() refilling the list.

One thing I cannot verify and would not want read as verified: enable_thinking:false is proven against this PR's own probe server (which speaks the shapes its docs describe), not against a real Qwen3 container through Lemonade on the box — device work was on hold. If Lemonade passes unknown fields through untouched, the c17 symptom (reasoning only, "0 answer chunks") survives this change. The c18 board boot is where that gets settled, and the row that would catch it is the chat-npu/follow-up family, not U3 webapp page (which counts a thinking= occurrence in the page source).

@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.

Correction first: my CHANGES on this PR was wrong about the cause, and the row is not disarmed on the surface the VM run used. I assumed the console buffer held what screen_text() prints; it holds what dialog reflows, and dialog collapses the runs of spaces. Your own evidence says so, byte for byte — ~/wt/os3i/g18-vm3.log, the U2 menu row, which dumps the whole tty1 screen:

api http://10.0.2.15:13305/api/v1 (key: menu item 1)                      <padding to the box width>

against the source's api http://10.0.2.15:13305/api/v1 (key: menu item 1). The Ä-rule characters and the fixed padding in the same capture are the dialog box, so the capture is the rendered screen, and the rendered screen has single spaces. So the single-spaced words are the correct ones for the menu surface, and the row passes it legitimately.

What is left, and it is one line of the same kind: after #921 (console TUI, READY) the surface tty1 gets is the TUI, which writes its own cells — ratatui does not reflow, so its api http:// and ssh root@ reach /dev/vcsa1 with both spaces, and a grep -F 'api http://' cannot match that. The missing- line prints and the row still passes, so the row's api/ssh assertions would be silently dead on the TUI surface, not on the menu one. Fix, and I would take both halves:

for w in CONNECT 'web UI http://' 'api +http://' 'ssh +root@' 'Lemonade Mobile'; do   # grep -qE, one space or more
    printf '%s' "$s" | grep -qE "$w" || miss="$miss $w"
done
[ -z "$miss" ] && echo menu-connect-block      # a missing word fails the row instead of being echoed

(or s=$(printf '%s' "$s" | tr -s ' ') before the loop, which is the smaller diff and keeps grep -F). Neither is needed for the merge the owner asked for — the row is honest about the surface it was measured on — but until one of them lands, its PASS does not mean the api and ssh lines were checked on whatever tty1 turns out to be after #921.

VERDICT: READY at b447ed8d3 under the owner's merge word, with that follow-up named so it is not lost. What I verified independently and stand behind: the CONNECT block renders on tty1 with all four lines and the :13305 address (your capture above, and I ran #921's side of the same block at 80×25 and 240×67); the no-address case is its own line pointing at menu 3; the desktop handback tries chvt 1, then busybox chvt 1, then says so and names Ctrl+Alt+F1; the qemu-budget guard (secs=${3:-600}, vm_alive, "not an image finding") is the right shape for the c17 false FAIL, and U3 inbound is an added assertion rather than a lowered one. The one thing I cannot verify and would not want read as verified is enable_thinking:false end to end: it is proven against this PR's own probe server, and whether Lemonade passes that field through untouched is settled only by the cycle-18 board boot.

@bong-water-water-bong bong-water-water-bong added the tprs-ready TPRS: review verdict READY posted by the standing reviewer; merge gate satisfied label Nov 3, 2026
@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

Persistent review updated to latest commit e9e1705

@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 e9e170520, merge verified rather than assumed. The new commit is Merge remote-tracking branch 'origin/main', and the one file that differs from the head I verdicted is 1bit-console-menu — which is exactly the file #921 edits (--action N), so it is the two PRs' changes in one file and worth checking by hand.

Both halves are there, in the merged blob:

--action)   [ $# -eq 2 ] || { say "usage: $0 --action <1..9>"; exit 2; }      # #921's arm, :589
    say "CONNECT  $(hostname_now)   web UI http://$ip:13305"                   # cycle 18's block, :222-225
    say "         api  http://$ip:13305/api/v1   (key: menu item 1)"
    say "         ssh  root@$ip   (your key in /data/ssh/authorized_keys)"
    say "         app  Lemonade Mobile: server http://$ip:13305 + that key"

screen_text/connect_text are intact (5 references), and sh -n is clean. The other three files are blob-identical to b447ed8d3, so everything in my READY stands.

The follow-up I posted is now owed rather than optional, because #921 merged an hour ago (25ca34fc3): tty1's surface is the ratatui TUI, which writes its own cells, so its api http:// / ssh root@ keep both spaces and vm-check.sh:466-467's single-spaced grep -qF cannot match them — while a missing- word only echoes, so the row stays PASS. grep -qE 'api +http://' / 'ssh +root@' (or tr -s ' ' the capture) plus a missing- word failing the row closes it. Until then, U2 menu connect's PASS does not cover those two lines on the surface the image now ships.

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown

Persistent review updated to latest commit 9c89eae

@bong-water-water-bong
bong-water-water-bong merged commit 5baf38a into main Nov 3, 2026
9 checks passed
@bong-water-water-bong
bong-water-water-bong deleted the os/v3-c18 branch November 3, 2026 19:34
bong-water-water-bong added a commit that referenced this pull request Nov 3, 2026
…busy), and no request URL carries a user value (#926)

* os/v3/puppy: the chat page retries the NPU's own refusal (503 device_busy), and no request URL carries a user value

Two follow-ups to #925, both in the page the appliance serves.

1. engine#924 bounds the wait for the one device: a request that cannot have it within --queue-ms is REFUSED with a
   status instead of parked silently, and Lemonade's proxy does not retry that on the request path - so the page is
   the only layer that can. It now retries after the advertised delay (Retry-After, clamped to 10 s, 1 s when the
   header is absent) up to RETRIES = 3, saying 'NPU busy, retrying (2/3) in 1 s' while it waits, and after the last
   attempt it stops with the engine's own words instead of hanging or reporting zero chunks.

   Both shapes #924 can produce are handled, because this page streams: a 503 + Retry-After + {'error':{'type':
   'device_busy'}} (status and headers reach the page through 1bit-webapp-proxy, which already forwards them), and -
   what the page actually sees for a streaming request - ONE 'data: {"error":{...}}' frame before 'data: [DONE]',
   which carries no type at all, so an error frame is retried when it says busy or when the stream had not produced
   a single chunk yet. A stream that fails after it produced content is reported, not silently requested again.

2. CodeQL, on the #925 diff: '?api=' made a fetch URL depend on a query parameter. It was a cycle-15 debug
   affordance nothing in the image uses (the launcher opens http://localhost:8080/ with no query string) and a way
   to make the appliance's own browser POST at another host, so it is gone: the paths are literal same-origin
   '/api/v1/...' and no tainted value reaches a request. ?model= / ?think= / the one-shot ?key= stay (none becomes
   a URL), and the key itself has not travelled in a URL since cycle 17.

Proven in a real browser against the two documented refusal shapes (~/wt/os3i/c18-page-probe.py, screenshots in
/tmp/busy-*): two streaming refusals then the answer; one 503 then the answer (status line reads 'NPU busy,
retrying (1/3) in 1 s' in both); nine refusals -> four attempts then 'NPU busy: still refused after 4 attempts (the
NPU is busy with another request) - press Send again in a moment'; and no refusals -> the answer unchanged. The probe
also caught the bug that would have shipped: the predicate was named busy and var busy=false overwrote it, so the
page answered 'TypeError: busy is not a function'.

U3 webapp page now requires device_busy and 'NPU busy, retrying' in the page the RUNNING proxy serves, printing
busy=N retry-text=N beside the other four counts.

* os/v3/puppy: the CONNECT row reads tty1 through tr -s, because two renderers draw that screen now

#921 merged the ratatui console TUI, and /etc/inittab's tty1 is the TUI when the image carries the binary and the
dialog menu when it does not (the documented fallback). They draw the same facts and differ in one way that matters
to a grep: dialog collapses runs of spaces in a --menu prompt, ratatui does not, so the TUI's screen says
'api  http://…' and 'ssh  root@…' with two spaces while the dialog menu's says one. U3's sibling row matches those
strings from tty1's VGA buffer, so it would have printed two misleading missing-* words on a TUI image (and passed
only because its marker keys on the single-spaced 'web UI http://' line).

Measured on both renders of the same fixtures: 80x25 TUI snapshot and the dialog menu at 80x25.

* os/v3/puppy: a refusal is retried only when the stream produced nothing, so a partial answer is never replaced

Copilot's review of #926, and it is right: the retry condition was `isBusy(j) || /busy/i.test(why) || !chunks`, so
(i) a busy-typed error after content was retried, and (ii) a *reasoning-only* stream (chunks = 0, rchunks > 0) also
passed `!chunks` - and engine#924's own streaming refusal is exactly a reasoning-less-but-something shape. A retry
clears the turn's two output elements, so either case silently replaced text the reader already had.

Now a refusal is retried only when the stream produced NOTHING (no answer chunk AND no reasoning chunk): that keeps
the c17 '0 chunks' recovery, which is what the retry exists for. A failure after any output is reported with the
engine's own words and the chunk count, and the partial answer stays on screen.

---------

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

Labels

Review effort 4/5 tprs-ready TPRS: review verdict READY posted by the standing reviewer; merge gate satisfied

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants