Day 3 — The full pipeline.
Stats heatmap. Mobile UI. Voice. Vision. Streaming. The SR8 server bootstrapped on the work machine and started pulling werbos on reboot. By the end of the day the pipeline was end-to-end: data streaming from the server to my computer AND my phone.
“i finally got the full pipeline working — data streaming from the server to my computer and my phone. the server auto-updates and pulls on reboot.”
That was the line that closed the day. Everything else — the heatmap, the mobile shell, the voice button, the paperclip, the overnight LoRA, the server in the corner of the office — lined up behind it.
What shipped
Order is roughly chronological: morning stats / training pipeline, afternoon mobile, evening server bootstrap, late-night streaming.
Morning — the dogfood loop got numbers
werbos statssubcommand +/statsslash command. Numbers I actually want to see: total sessions, turns, active days, longest / current streaks, longest session, estimated tokens, most active day, favorite tool, game-type + outcome breakdowns. (874b67b)- 365-day GitHub-style heatmap. Full year window, glyph density
matched to actual session counts. (
06f9460) - Verdict roll-up surfaced in stats. The
+/-thumbs that ship with every assistant message now report as{good, bad, skipped, unlabeled}— the dogfood loop has measurable output. (4145b13) - v4 corpus filter + split pipeline. Two scripts —
filter_claude_code_corpus.pyandsplit_corpus.py— that walked~/Desktop/claude-projects-backup(115K Claude Code events) and produced 1,265 clean (user, assistant) pairs at~/werbos/training/lora-v4-data/(1138 / 63 / 64 split). (6e1ddae)
Afternoon — mobile shell, idle finalization, voice + vision
- Mobile-responsive UI. Drawer for chat list, settings panel,
theme toggle (light / dark / werbos-sage). The same React app now
works at phone width inside Tauri Android. (
2a3e2ba) - Idle-session sweeper + end-chat button. Background
asynciotask in the Starlette lifespan handler finalizes any session that hasn’t seen a turn in 15 minutes; the UI gets an explicit end-chat affordance for clean closes. The fix is load-bearing for the dogfood loop — sessions the user walked away from used to stick in memory and never write a.pfm. Now the on-disk archive is complete by construction. (f798386) - Voice chat. Web Speech API only (
SpeechRecognition+speechSynthesis); no new backend deps. Mic button on the composer dictates into the draft, “speak replies” toggle in settings reads assistant messages aloud. Works on the phone over Tailscale. (9c22a0a) - Vision messages. Paperclip on the composer attaches up to four
images per turn. Server strips data-URL prefixes, caps each at
~10 MB, forwards as base64 to
UserMessage.images. Vision-capable backends (llama3.2-vision) consume them; text-only models silently degrade to the caption. (24ce248)
Late evening — streaming, end-to-end
BellmanBackend.stream()— real Ollama NDJSON streaming. Maps content deltas →TokenEvent, structured tool calls →ToolCallStart/Args/End,done_reason: length→max_tokens. Salvage parsing intentionally skipped on the streaming path: by the time we’d know JSON-shaped prose was a tool call we’d have already streamed the tokens out. Callers who need salvage stay ongenerate(). (d92cc7c)Agent.run_stream()— streaming twin ofrun(). Relays backend events while reconstructing theAssistantMessagein the background, dispatches tool calls between iterations, same termination metadata as the non-streaming path. (d92cc7c)ChatSession.step_stream→ HTTP?stream=1→ React client. Live tokens in the UI bubble with a blinking caret. The bulk JSON path stayed for the MCP bridge and curl scripts. (d92cc7c)
Throughout the day — security Round 2 closeout
11 R2 findings audited and fixed: SSRF guard on web_fetch, exponent
cap on the calculator tool, archive-id traversal, importer path
escape, CORS wildcard rejection, MCP provenance tagging, real DNS
pinning that closes the TOCTOU on web_fetch, tightened
is_safe_archive_id, CORS whitespace handling, and SHA-pinning every
sibling dependency. (commits b110765 → 009dadf)
SR8 — the server in the corner
The work machine (AMD Ryzen 7 255, Radeon 780M, 28 GB RAM, Ubuntu
24) became sr8 — the always-on werbos server. Tailscale is
the only networking. werbos serve runs natively (no Docker), the
React app is built once and served from the same origin so CORS is
moot, and a small reboot-time script git pulls the four siblings
- werbos and restarts the systemd unit. From the Mac and from the phone, the chat is the same chat — same session ids, same archive, same model.
That’s the headline of the day. The chat that started on my laptop
during lunch finished on my phone on the couch, and the .pfm for
that session has both halves.
v4 LoRA — the lesson the metric tried to hide (again)
v4-3B trained successfully overnight on the 1.3K Claude-Code pairs; the loss plateaued at val 3.091 by iter-200 and oscillated for 1000 more iters without improving. Smoke test (a HUGO question) returned generic “let me check the web search first” prose — not recognizably me. The 3B base + small corpus produced too marginal a behavioral shift.
v4-8B was attempted in a fresh terminal: OOM at iter 1. 16 GB on this M-class is a hard ceiling for 8B + LoRA activations, period. Adapter dir created, no checkpoint inside.
v4-8B is not pending. v4-8B is abandoned. Default to 3B-base for v5+ until hardware changes.
Numbers worth keeping
| Metric | Value |
|---|---|
Day 3 commits to werbos repo | 15 (stats x3, mobile, idle, voice, vision, streaming, security R2 x7) |
| LoRA pairs filtered out of 115K Claude Code events | 1,265 |
| Train / valid / test split | 1138 / 63 / 64 (90 / 5 / 5) |
| v4-3B best val loss | 3.091 (iter 200, plateau) |
| v4-8B best val loss | — (OOM at iter 1) |
| Hard ceiling for 8B LoRA on 16 GB M-class | confirmed |
| Security R2 findings closed | 11 of 11 |
| Idle session timeout default | 15 min |
Streaming events in the closed StreamEvent union | 5 (token, tool_call_start, tool_call_args, tool_call_end, done) |
| Streaming layers wired | 6 (Bellman, Agent, ChatSession, Registry, HTTP, React) |
| Streaming tests added | 9 (4 Bellman, 4 Agent, 1 HTTP) |
AHAs
- The dogfood loop is real now. Stats, verdicts, the per-message
thumbs, the
.pfmarchive, the heatmap. Numbers I look at, not numbers I imagine. - A “session” only exists if it lands on disk. The idle sweeper is the load-bearing fix for that. The phone has no obvious “end chat” event; without the sweeper, half the corpus would have been ghost.
- Streaming is not a UI feature, it’s an architectural decision.
The closed
StreamEventunion, the salvage-skip note, the per-session lock held for the duration of the stream, the syntheticDoneEventon truncation — each one is a small invariant that makes the next one tractable. - One server, two clients, one archive. The session continuity
between Mac and phone — same session id, same
.pfm— is the moment werbos stopped being a CLI tool with a phone companion and became one assistant with two faces. - Hardware ceilings teach faster than benchmarks do. v4-8B failing at iter 1 on a clean session is a number I won’t argue with. v5+ goes 3B until the hardware story changes.
Quote of the day
“i finally got the full pipeline working — data streaming from the server to my computer and my phone. the server auto-updates and pulls on reboot.”
The line that closed Day 3. The thing every previous day was quietly building toward.
What’s next
- v5 specialist LoRAs on 3B-base (debate, fun-fact, scripture —
three voice-distinct personalities, hot-swapped via
--adapter). - Streaming on the CLI surface for parity with the .ai app.
- Bench shootout: stock-3b vs each specialist on TTT / code / debate.
- Fifth voice-specialist trained on HUGO content (50+ sites of Jason’s own writing).
- Per-message label loop → v6 retrain of the strongest specialist on accumulated thumbs.