Day 5 — 'I think that is a glass of beer.'
Image upload shipped. Sprint A delivered vision routing end-to-end on the phone, after a long sequence of layered bugs (Ollama tool-call hallucinations, message-ordering breaking the chat template, model narrating tool internals, hardware speed walls). The line 'i think that is a glass of beer' on a phone running on local hardware closed the multimodal loop. Plus: mobile scroll fixed in three CSS lines, end-chat history fixed, new logo, and a long evening of design conversations that grew the BACKLOG considerably.
“i think that is a glass of beer”
That sentence — generated by llama3.1:8b on SR8, after moondream
described a phone-camera photo, with the description injected as a
SystemMessage that the agent reasoned over — closed the multimodal
loop on local hardware. Photo on phone → vision module → text model
with vision context → streamed reply on phone. End-to-end, on
your hardware, with your weights, from a single photo of a beer in
a “burning barrel” glass.
The sentence is small, slightly funny, and the actual artifact of the day. Everything else built up to it or fell out of it.
Morning — clearing the post-Day-4 punch list
Day 4 night left several small bugs visible. Day 5 morning closed each:
- Mobile chat scroll (
3894fc6). The100dvh+overflow- wrap: anywherefix from Day 4 night had landed but mobile chats still wouldn’t scroll. Diagnostic plan in BACKLOG section 7 said “step 2: suspect the flex chain above.chat-scrollis losingmin-height: 0somewhere.” Exactly that. Three CSS lines:min-height: 0,height: 100%,overflow: hiddenon.main, plusgrid-template-rows: 100dvhon.app. Mobile scroll works. - End-chat doesn’t persist in history (
871598b). CLI’sarchive_dirdefaulted toNonewhen--archive-dirwasn’t passed; the listing endpoint then skipped the archive directory entirely. MeanwhileChatSessionwas happily writing to~/werbos/chats/. Read and write defaults were silently mismatched. Default both to~/werbos/chats/. - New logo (
c3b74ed). Sage-green graph-W (a small nod to Paul Werbos, who invented backprop in 1974). Tauri’snpx tauri iconregenerates every platform-specific size from one master; HUGO site picks up the same favicon. Placeholder artwork; the master atwerbos/ui/src-tauri/icons/source-1024.pngmakes future regenerations a one-line command. - React-key warning + console 404 noise (
69b47b0,07a370a,c77ddb8). Dedup the voice list (macOS exposes multiple “Samantha” entries with the samevoiceURI); make the runs-router fall back to active sessions when an in-flight chat hasn’t archived yet (sogetRun()doesn’t 404 on every fresh chat); addPUT+PATCHto the CORSallow_methodsso labeling endpoints work fromlocalhost:5173cross-origin.
Afternoon — Sprint A: vision routing, three phases
Per BACKLOG section 3, “vision-as-internal-context” was the
locked design: vision model produces a description, ChatSession
injects it as a SystemMessage, the text+tools model owns the
user-facing turn. User sees one stream of tokens; the vision
pass is invisible.
- Phase A.1 — VisionContextProvider + CLI flags
(
d8e71ed). Newwerbos/backends/vision.py. Wraps any pitts.Backend whose model acceptsimageson a UserMessage. One method,describe(). Three new CLI flags onwerbos serve:--vision-model,--vision-host,--vision-prompt. Default off when blank. 10 unit tests pin the contract. - Phase A.2 — ChatSession vision interlude + memo cache
(
8107283).stepandstep_streamrun the interlude between human turn and AI turn when (a) latest UserMessage carries images and (b)vision_provideris configured. Per-session memo bysha256(sorted-image-b64s + user_text)skips re-running vision for repeat attachments. NewKIND_VISION_OBSERVATIONrecorder event with newACTOR_VISION(intentionally notACTOR_SYSTEM— the SFT exporter needs to filter vision events independently). 9 new tests. - Phase A.3 — Flip the paperclip back on, rebuild APK
(
7077a18).IMAGE_UPLOAD_ENABLED = trueinChatView.tsx, rebuild Tauri Android, extract APK via bundletool, copy to the Syncthing drop-off path. SR8’sstart-werbos.shupdated to include--vision-model llama3.2-vision:11b(later swapped tomoondreamper the speed story below).
90 minutes of focused work, three commits, working feature on the phone.
Evening — the long sequence of vision bugs
Then four hours of “the demo doesn’t work; here’s why” iterating through layered failure modes:
- Hardware reality check.
llama3.2-vision:11bon SR8 (Ryzen 7, no GPU offload) is genuinely slow on CPU — first phone test ran 160s with no output.ollama psshowed both models loaded at 100% CPU. Switched tomoondream(~2GB, designed for fast captioning); inference dropped to ~10s. Vision-grade quality vs caption-grade — for “describe this so the text model can answer about it” the trade is fine. - Phase events + 90s timeout + UI placeholder swap
(
ad99227). The vision interlude was invisible during the call: the bubble showed “Thinking…” for the entire 30-120s with no signal. AddedPhaseEvent("vision", "Looking at the image...")at the start of the interlude andPhaseEvent("thinking")after, plus a hardasyncio.wait_for90s timeout that emitsPhaseEvent("vision_failed", <reason>)and proceeds text-only on overrun. UI swaps the placeholder text from the rotating loading line to the phase message. Failures are now visible even when they happen. - Directive SystemMessage (
5ea587c). First successful vision call with moondream on the phone still produced an empty AI reply. Diagnosis from the .pfm chain: vision succeeded (correct beer description) but the AI turn recordedAI_TURN_START → AI_TURN_ENDwith NO chat content. The SystemMessage shape was meta-flavor ((Vision module saw, attached to user's last image: ...)) — small instruct-tuned models read parenthetical asides as background and don’t act on them. Rephrase as directive:[VISION CONTEXT] The user has attached an image. Vision module describes it as: <X>. Use this description to answer the user's question. - Message ordering (
4199383). Empty AI replies persisted. Live repro against SR8’s Ollama isolated the issue: the chat template (Llama 3.1) breaks when a SystemMessage comes AFTER a UserMessage in the history. Direct curl withsystem → user → systemproducedeval_count: 1and empty content. Same curl withsystem → system → userproduced a coherent reply._run_vision_interludewas appending the SystemMessage AFTER the user’s UserMessage; fix: insert it BEFORE. One index change in the history-mutation logic. - Tool-internals leak (
88d8d85). Vision now worked; AI produced a real reply. But the model also tried to userun_commandto read EXIF from a fake/path/to/image.jpgit imagined existed. The shell tool returned 127 (“identify: not found”) and the model narrated the failure to the user. Right answer, embarrassing narration. Extended the[VISION CONTEXT]SystemMessage with explicit guidance: “The image is NOT available to you as a file or URL. Do NOT attempt to read or inspect the image with run_command, web_fetch, or any other tool — those will fail.”
After the fourth fix landed, the phone test produced “i think that is a glass of beer” on the next try. The dream stopped being theoretical.
Late evening — the voice loop
After vision worked, the next demo was voice: dictate a question into the composer, send to werbos, hear the reply read aloud.
- Mac browser test in Brave: failed. Mic recorded for ~250ms
then dropped silently. No console errors, no failed network
calls, just a fading red indicator in the tab bar. The hook’s
no-speechfilter (intended to hide harmless “user said nothing” events) was masking the actual cause: Brave Shields blocks the Web Speech API by default because Chromium’s implementation routes audio to Google’s cloud STT servers. - Chrome A/B confirmed it. Same Mac, same Vite dev server, same hardware — Chrome’s mic worked end-to-end. Voice input, werbos’s text reply, TTS reading the reply aloud, full loop. Brave Shields was the entire problem.
- Silent-block detection (
33daa8c). Made the failure visible. Hook now tracks whether any result event fires during the session; ifonendfires within 1s ofonstartwith no result, populatestt.errorwith a clear actionable message naming Brave Shields specifically and listing alternatives (Chrome, Safari, disable Shields). ChatView now also RENDERSstt.errorin the composer area — previously the error was being set but never displayed to the user, so every failure looked invisible. - Phone APK still shows “Speech recognition not supported”
because Tauri Android WebView genuinely doesn’t expose the
Web Speech API. That’s a known WebView limitation, not
something werbos shipped wrong; the real fix is a Tauri
plugin wrapping native Android
SpeechRecognizer. Out of tonight’s scope. Now at least the failure is visible to the user instead of a silent broken mic icon.
End state: voice → text → werbos → voice working end-to-end in Chrome on Mac. Two multimodal pipelines closed in one day.
On hardware honesty
“in 2026 we are just used to INSTANT”
Local-first vision on a CPU-bound box is not instant. moondream takes 8-15 seconds to caption a phone photo. The text model takes another 5-10. That’s a 13-25 second turn for image queries on this hardware tier. The dream isn’t bad, it’s unevenly good: text chat is real-time; tool calls are sub- second; vision is a deliberately-slow lane because that’s where the hardware ceiling actually bites.
Three real paths forward whenever the slowness becomes unacceptable: hardware refresh (used 3090 ≈ $700 turns vision 60s → 2s), Vulkan backend in Ollama (mixed iGPU support, worth a 30-min experiment), or just hide the paperclip again with one flag flip and revisit when the hardware story improves. None of them tonight.
Backlog growth
A long evening of design conversations grew the backlog considerably — continuing to add more ideas that will shape how users control and use werbos. Captured but not detailed here; the BACKLOG file in the repo carries the specifics.
Numbers worth keeping
| Metric | Value |
|---|---|
Day 5 commits to werbos repo | 28 (mobile, logo, vision sprint A x3, vision bugs x4, voice fix, console-noise x3, CORS, etc) |
| Multimodal pipelines closed | 2 (vision + voice loop) |
| Sprint A phases shipped | 3 (VisionContextProvider, ChatSession interlude, UI flip) |
| Vision-interlude bugs found and fixed in evening | 4 (timeout/phase, directive prompt, message ordering, tool-internals leak) |
| Live phone test from “0 result” to “glass of beer” | ~4 hours, 4 commits |
| Mobile scroll fix | 3 CSS lines |
| Tests passing end of day | 909/909 (werbos) + 133/133 (pitts) |
| Hardware speed reality on Ryzen 7 + iGPU + Ollama CPU mode | llama3.2-vision: 60-120s/call. moondream: 8-15s/call. |
| Voice loop browser compatibility | Chrome ✓, Safari ✓, Brave ✗ (Shields blocks STT), Tauri Android ✗ (WebView lacks API) |
| BACKLOG sections that grew | many; specifics in repo not log |
AHAs
- The 100dvh/overflow chain has many ways to break. Mobile
scroll required
min-height: 0ANDheight: 100%ANDoverflow: hiddenon.mainANDgrid-template-rows: 100dvhon.app. Any one missing and the scroll silently fails. Captured in BACKLOG section 7 with a 4-step diagnostic plan before the fix landed; the plan accurately predicted step 2 was the issue. - Read and write defaults must agree. End-chat-doesn’t-show was a one-line fix at the read site (default the archive directory). The bug was invisible because both halves worked in isolation; it only surfaced when the user expected closed chats to appear.
- The vision interlude has four distinct failure modes. Hardware-too-slow, no-UI-feedback, model-doesn’t-read-the- context, model-tries-to-use-tools-on-the-image. Each one required a different fix at a different layer. None were predictable from the design doc; all were obvious in hindsight. The .pfm chain was the indispensable diagnostic tool — recorder events made each turn’s actual behavior inspectable post-hoc.
- System message ordering matters in chat templates. Llama
3.1’s chat template breaks on
user → systemordering and produces 1 EOS token. This is not documented anywhere obvious; the bug was found by curl-A/B-testing the same payload with system before vs after the user message. Pin the lesson: when injecting context via SystemMessage, ALWAYS insert before the related UserMessage, never after. - Caption-grade vision is plenty for “use this in the next agent turn.” moondream’s “the image features a glass of beer with ‘burning barrels’ written on it” is enough for llama3.1:8b to give a coherent answer. The vision model doesn’t need to be the smartest one; it needs to be the fastest reliable describer. Picking a vision model is a speed/quality tradeoff that should default to speed once quality clears a threshold.
- The dogfood loop’s
badlabel catches narration bugs, not just factual bugs. The “model leaked tool-call internals to user” turn was technically correct but unprofessional. Labeling itbadis exactly the right signal even though no fact was wrong. Pattern failures in voice/style are first-class targets, not also-rans.
Quote of the day
“i think that is a glass of beer”
Said by werbos to Jason, on Jason’s phone, on Jason’s hardware, about Jason’s beer photo. The smallest possible sentence that proves the multimodal pipeline works end-to-end. Everything that took multiple commits to fix made this one sentence possible.
What’s next
- Sprint B candidate: per-move labels in TTT GameView (per BACKLOG section 5.1.2) — extends the dogfood loop’s signal shape from chat preferences to game-move correctness, which has an objective oracle (minimax) for cross-validation.
- The ingestion pipeline for the personal corpus (BACKLOG section 5.1.4) is plausibly the highest-leverage infrastructure to build before any v6+ training run.
werbos init+.werbos.md(BACKLOG section 9.1) is the smallest-possible memory feature — single sprint, immediate value in any project directory.- The mirror slider (BACKLOG section 10.7) is testable as an MVP without the full constitution+character infrastructure — even just blending two persona files at a ratio is a working demo of the idea.