Repository navigation
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
Conversation
…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.
PR Reviewer Guide 🔍(Review updated until commit 9c89eae)Here are some key observations to aid the review process:
|
|
#925 ready for review at b447ed8 — cycle 18: chat page answers with |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
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:
- restore the two-space words (
'api http://','ssh root@') — or normalise:s=$(printf '%s' "$s" | tr -s ' ')before the greps; - make a missing word fail the row:
miss=instead ofecho 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),
CONNECTcarries the hostname,apipoints 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-106trieschvt 1, falls back tobusybox 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:falseby default with the "show thinking" switch,max_tokens1024, the picker filtered todownloadedwith a GPU/NPU label, and the state fix forask()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
left a comment
There was a problem hiding this comment.
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.
|
Persistent review updated to latest commit e9e1705 |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
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.
|
Persistent review updated to latest commit 9c89eae |
…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>
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 theone 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 onlydelta.content, so thevisible answer stopped mid-sentence and the follow-up printed
done: 0 chunks in 5.9 s. The page now asks forenable_thinking: falseby default (Lemonade's own extension —bin/resources/docs/api/openai.md:55; thechat_template_kwargsspelling in the brief is the MCP planner's knob, not this one), keeps a "show thinking"switch that sends
trueand renders the reasoning dimmed above the answer, raisesmax_tokens512 → 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 todownloaded(a catalogue entry that is not on the box wouldturn a demo into a 1-4 GB download), labelled GPU / NPU from the model's own
recipe_options.onebit_backend(with a
-NPU-1bitid 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, theOpenAI-compatible base
http://<ip>:13305/api/v1with the key pointed at menu item 1 (never printed on a screena recording catches),
ssh root@<ip>, and Lemonade Mobile's server URL — and while the wire or Lemonade is stillcoming 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 —
--statusstill 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-desktopstarts 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 theboot'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 PaleMoon under Xvfb:
The probe caught a bug of mine that reading the diff would not have:
ask()refilled the model list before everyprompt 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 itemsdrawn 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 thedialog 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 at3a428849, payloadlemonade-onebit-rc-2026-11-03-c21(engine0b04696e…), libhsaf6ef689d…, kernel7.3.0-rc4-1bit-perfopt+(out-c16). pi-release has verified the sha256 from their own read of the file.1bit-os-puppy.img(ESP 512 MB +1BIT-DATA8 GiB)22c60ec4f506d1eacad970f87614bbb9d13e5d61e247221ef5505e3f5a956bd1upup64r-26.04.iso(side artifact)884dceec0a985bcd65d114c98b2d8cc54e3c32d1fdb1c20acc0de3446350073fbuild.sh resolute64ok:checks (packed-initrd guard, packed-SFS uid/gid scan, console menu executable,ADDLIST=ec_su_axb35)mkimage.shunsquashfs -caton the puppy SFS: menu carriesCONNECT/screen_text/spinner, page carriesenable_thinking/reasoning_content/MAXTOK = 1024, desktop carries thechvt 1returng3-probe.sh --cert ~/wt/mok-kernel.pem --require-mok/vmlinuzverifies against the enrolled MOK, 3780 modules,amdxdna(fromupdates/) +ec_su_axb35signer and vermagic, firmware presentg2-probe.shprepare-stick.shon a copyb076cd…,id_c15testkey inauthorized_keysvm-check.shon the prepared copy, Secure Boot on~/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 fileCycle-18 rows from that run:
One harness defect found by the first gate run, and fixed here
The first
vm-checkrun on this image reportedFAIL U3 lan http=000andFAIL U7 ssh connection refusedwhileevery in-guest row passed, sshd listening on
0.0.0.0:22andhosts.allowcarrying the LAN line included. Thetwo 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 inboundwas added, so the firewall/listener/hosts.allow mechanism behind those two rows is measured instead of assumed.
Not claimed
/data/models/Qwen3-0.6Band 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.avahi-daemonis not in this image (measured in cycle 15), so the block prints the hostname and theaddress, not
1bit-os.local. The same block belongs in pi-tui's TUI header — another lane's file(
~/briefs/TCONSOLE-TUI.md).board's TDR appeared, so adding history is a separate decision, not a silent change.