Day 4 — Two halves: closing the demo loop, then naming the orchestration plane.
Morning closed Day 3's bad-demo recovery: salvage fixes, mobile CSS, vision model triage, image cap bump. Evening was a deep architectural pass that ended with five siblings drafted: observe (shared see-layer), watchtower (summon), warden (defend, planned), beacon (signal, planned), and a standalone SPA wrapper. werbos got its first BACKLOG.md.
“Smallest, most secure, timeless, protocol we can make.”
The design directive that shaped the second half of Day 4. By the
end of the night, the orchestration plane had a name (observe),
a flagship consumer (watchtower), two planned siblings (warden,
beacon), three new repos, two SPEC drafts, and an Archaeologist
review verdict of DURABLE.
The morning was much more practical: closing every blocker that showed up during yesterday’s bad demo.
Morning: closing the demo loop
The Day 3 night marathon ended with streaming working but image upload broken on the phone. Day 4 morning fixed each remaining blocker in turn.
The bugs that fell
- Empty-
{}placeholder strip (107adb2in pitts). Stock llama3.1:8b sometimes opens replies with a literal{}followed by self-aware narration (“Note: the question doesn’t require a tool, providing an empty response.”). Salvage parser correctly ignored the empty dict (nonamefield), but didn’t strip it either, so the user saw raw JSON in the bubble. Bug captured in session907897d64d93. Fix: post-process leading{}if no tool calls were salvaged. Mid-content{}is preserved (e.g. “empty dict in Python is{}”). werbos serveregistered no tools by default (2eac612). Web search/fetch and shell tools requiredargs.no_search/args.no_shellto exist on the namespace; the serve parser didn’t define those flags, sohasattr(args, ...)returned False and the tools never registered. Fix: mirrorwerbos chat’s flag set in serve. Bug captured in session46c4dee93de8(“when is the best time to shore fish in Cabo?” returned the literal text-form tool call JSON instead of executing).- Mobile-header was the wrong selector (
2ca6a84). The 35px safe-area-inset padding I added on Day 3 night landed on.chat-header(the chat-title subheader) instead of the actual top bar with the hamburger and cog. Move the safe-area fix to.mobile-headerand revert chat-header to its original padding. - Bubble wrap + viewport (
db385c2). Long URLs from web search results forced horizontal overflow. Mobile chat-scroll wouldn’t actually scroll because100vhdoesn’t track the WebView’s actually-visible area when system UI shifts. Fix:overflow-wrap: anywhereon bubbles,min-width: 0on flex ancestors,100dvhon the app root, viewport-fit=cover on the meta. Inspired by Cheng Lou’s pretext markdown-chat experiment (his approach is fully virtualized; the load-bearing CSS primitives translate cleanly to flow layout). - Image cap bump (
0f376f3). The 10 MB-per-image cap was tripping legitimate phone-camera shots; raised to 30 MB. Lifted the SR8 in-place sed hotfix into mainline so SR8 stops diverging ongit pull. /phone-api/*HTTP proxy (74e71b7). The phone bridge runs on SR8 alongside werbos serve; the React UI calls/phone-api/callsthrough werbos serve (proxied to localhost:9000 via stdlib urllib in asyncio.to_thread). One origin, no separate CORS surface, one less runtime to keep alive.- Optional games-routes import (
12e6d8a). SR8 was crashing onwerbos serveboot becauseapp.pyimportedwerbos.server.gamesbut the module was uncommitted WIP on the Mac. Wrap in try/except so app.py boots cleanly without it; once games.py is finalized, the routes get picked up automatically.
v5-ttt LoRA + MLX endpoints landed
9ecef8c and 84b1cf4 from earlier in the morning shipped the
v5-ttt rationale corpus generator and the /api/games/ttt/move
MLX-backed endpoint — the v5-ttt LoRA loaded onto Llama-3.2-3B-4bit
serves moves to the React GameView. First piece of an actual game
training pipeline running end-to-end.
Vision triage
llama3.1:8b doesn’t see images. llama3.2-vision:11b sees images
but Ollama returns 400 if you send tools to it. Picking one means
losing one feature. Day 4 punted: stay on llama3.1:8b for tools
support; multi-model routing (vision when an image is attached, text
otherwise) goes on the streaming-followups backlog.
The win moment
“UI IS CLEAN!!!!!”
When the rebuilt APK landed via the Syncthing pipeline (Desktop/APK_DROPOFF/Werbos/werbos.apk is now a deploy target — the Mac drops a build, the phone receives via Syncthing, install locally; no USB, no adb), the mobile header cleared the camera, the composer cleared the home indicator, photos uploaded, search executed via end-of-stream salvage. Six checkpoints from the rebuild plan, all green.
Evening: naming the orchestration plane
The afternoon question was small: “24/7 background agents that yield to humans?” The answer to that question — built up in conversation across two hours — became a five-sibling architectural plan.
werbos/BACKLOG.md first
Created the project’s first backlog file (0fd700b). Eight
sections: background agents, watchtower, streaming/UX, phone bridge,
training, infra, mobile UI, dev tooling. Status legend
([idea] / [planned] / [in-progress] / [shipped]). Recently-
shipped section near the bottom for cross-reference, prunes after
a week or two.
Background agents named as a concept (8685f15)
The defining property is yielding: a background agent runs when nothing else is using the model and gets out of the way the instant a foreground turn arrives. Cooperative, not preemptive. This distinguishes it from cron (which fires regardless of state) and from foreground turns (which always win). Once “background agent” is a noun in the system, every autonomous idea is just an instance of it.
Five orchestration primitives surfaced:
- Foreground-busy signal —
SessionRegistry.is_foreground_busy() - Background work queue — asyncio task draining a FIFO of work items
- Pending-actions queue — destructive ops require human approval
- Claude budget gate — daily cap on Claude API calls
- Shared task memory (
86316cf) — SQLite table that lets one agent fill in the next
The pending-actions queue is the load-bearing safety primitive. Without it, autonomous werbos is scary. With it, autonomous werbos is a quiet apprentice handing you a daily clipboard of “approve to act.”
Watchtower as the 5th sibling (480a9b6, 749f498, 9084b27)
The decomposition was too clean to ignore:
| Sibling | Verb | Concern |
|---|---|---|
| pitts | think | agent loop |
| mcculloch | judge | eval |
| partyparrot | talk | real-time wire |
| pfm | persist | archive |
| watchtower | summon | triggers + dispatch + monitoring |
Watchtower owns one thing: see the world, decide when to ring the bell. It does not know what agents think (pitts), how they’re scored (mcculloch), or where they archive (pfm).
Two use cases the same protocol covers: productivity (background agents fire on schedules / events) AND defense (active security monitoring — the same “ring the bell” shape). Static defenses prevent; watchtower detects when something gets close.
Architectural commitments locked in:
- Protocol first, runtime second. Same as partyparrot. Spec is primary; the Python reference runtime is one implementation.
- Universal — never assumes werbos. Dispatches via three generic primitives: HTTP request, shell command, in-process function call. werbos serve happens to be a frequent HTTP target; nothing in the protocol names it.
- Standalone SPA wrapper.
watchtower-uiis its own deliverable, not a tab in the werbos chat app. Configurable backend URL. - Three deliverables, three repos:
watchtower(protocol + reference Python),watchtower-ui(Vite SPA),watchtower-py(future, if a non-Python runtime ever happens — mirrors partyparrot/partyparrot-py).
The full SPEC.md draft + Archaeologist review
Wrote a complete protocol skeleton: resources (trigger, dispatch,
pending, task, audit, alert), endpoints (/v1/*), state machine
for trigger-fire, threat model, versioning policy, open questions.
Then handed it to the Archaeologist agent for the 25-50 year
survival review.
Verdict: DURABLE. Three blockers needing fix before v1.0 lock:
- ULID → UUIDv7 (RFC 9562). ULID is not in IETF/ISO; UUIDv7 is ULID-shaped (time-ordered, lexicographically sortable) AND standardized. A 2050 archaeologist parses UUIDv7 cold; ULID requires finding a GitHub README that may not exist.
- Pin shell allow-list match semantics. Argv-prefix equality; substring/regex/glob explicitly forbidden; default-deny on empty allow-list. As written, a careless 2030 implementer could soften to substring match and call it spec-compliant.
- Pin canonical-JSON to RFC 8785 (JCS). Otherwise two
conformant runtimes compute different
selfHashvalues and the audit chain breaks across them.
Plus smaller fixes: emit the magic identifier on the wire (in
/v1/version and the WS/SSE hello frame), specify webhook
HMAC-SHA256 + constant-time compare, IANA tzdata required for
schedule timezones, content-negotiated WS-or-SSE for the events
stream, default body cap 1 MiB with 413 on overflow, threat-model
expansion (“untrusted-network deployment requires mTLS or
equivalent”).
All folded in. Watchtower SPEC went DRAFT-1 → DRAFT-2 in one editing pass.
The suite split
“What if we make this a suite? Watchtower… Lighthouse and Sentry… one might look for tasks and delegate, one only looks for hacks, bad data… and the other would build and support.”
The instinct held up. Three siblings, each “sees” something but with distinct emit paths:
| Sibling | Sees | Emits |
|---|---|---|
| Watchtower | tasks, schedules, triggers | dispatch actions |
| Warden (planned) | logs, traffic, file-state, allow-list violations | alerts + auto-defense |
| Beacon (planned) | builds, deploys, health endpoints | status + notifications |
Naming dodged the famous-tool collisions: “Sentry” (sentry.io, error tracking SaaS) and “Lighthouse” (Google’s web auditor) are both heavily indexed. Warden and Beacon are unambiguous and fit the security/observation theme already in the project’s vocab (Mr BlackKeys, Specter, Hardcoder, etc.).
The shared see-layer extraction — observe
If three siblings all need to describe what they’re watching using the same vocabulary, you don’t want three slightly-different versions of identical logic. The shared bits got pulled out as a new sibling.
observe is the see-layer primitive — schema + library, no
service of its own. DRAFT-1 covers:
- Source envelope (
{protocol, type, config}) withcron,fs,predicate,webhookvariants - Closed predicate operator language (
eq,ne,lt,lte,gt,gte,in,not,and,or; no DSL escape hatch, no arithmetic, no string ops) - Webhook auth — HMAC-SHA256 + constant-time compare
- Shared wire conventions — UUIDv7, ISO-8601 UTC, IANA tzdata, RFC 8785 canonical JSON, newline preservation, 1 MiB body cap
- ForegroundBusy interface signature
- Error envelope (
{error, detail, fields?})
Watchtower’s SPEC went DRAFT-2 → DRAFT-3 in the same editing
pass: section 0 (Conventions) and section 2 (Errors) point to
observe instead of redefining; section 3.1 (Trigger) embeds an
observe Source instead of duplicating per-type schedule schemas.
Watchtower SPEC shrunk by ~80 lines while gaining clarity.
Shape of the suite at end-of-day
┌─────────────────┐
│ observe │ shared see-layer
│ (cron, fs, │ schema + library
│ predicate, │ no service
│ webhook, │
│ predicate │
│ ops, HMAC, │
│ conventions) │
└────────┬────────┘
│
┌──────────────────┼──────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ watchtower │ │ warden │ │ beacon │
│ summon │ │ defend │ │ signal │
│ (DRAFT-3) │ │ (planned) │ │ (planned) │
└──────────────┘ └──────────────┘ └──────────────┘
Three new repos initialized
~/Documents/Companies/werbosLLM/observe/— README + SPEC, git-init’d~/Documents/Companies/werbosLLM/watchtower/— README + SPEC, git-init’d~/Documents/Companies/werbosLLM/watchtower-demo/— README, git-init’d
No remotes yet. They’ll be gh repo create’d before any runtime
code lands. Per the Day 4 design directive, no implementation work
happens tonight — just the design, pressure-tested, written down.
Numbers worth keeping
| Metric | Value |
|---|---|
Day 4 commits to werbos repo | 19 (mobile fixes ×7, serve fixes ×3, BACKLOG ×7, ttt ×2) |
Day 4 commits to pitts repo | 1 (107adb2 empty-{} strip) |
| Day 4 commits to other siblings | 0 (phone, mcculloch, partyparrot, pfm untouched) |
| New sibling repos initialized | 3 (observe, watchtower, watchtower-demo) |
| Lines added to BACKLOG.md | ~600 |
| Watchtower SPEC drafts | DRAFT-1 → DRAFT-2 → DRAFT-3 (one day) |
| Archaeologist verdict | DURABLE with 3 blockers (all folded in) |
| Famous-tool naming collisions dodged | 2 (Sentry, Lighthouse) |
| Sibling primitives now in the family | 6 (pitts, mcculloch, partyparrot, pfm, observe, watchtower) — was 4 |
| Hours of design work that produced no code | ~3 (and that’s a feature) |
AHAs
- The pending-actions queue is the trust foundation for autonomous werbos. Same compute, completely different relationship: without it, “werbos deletes my files while I sleep”; with it, “werbos hands me a daily clipboard of approve-to-act.”
- The most ship-able thing in this whole project might be watchtower. Chat assistants are crowded; PFM archives are niche; LoRA personality training is researchy. “A protocol + open-source SPA for managing background AI agents that yields to your foreground work, with a pending-actions queue so it can’t run wild” — that’s a thing nobody else has cleanly packaged.
- Watchtower is the agent-management plane; werbos serve is the runtime plane. That’s how Kubernetes is organized (control vs data plane), how AWS is organized, how Tailscale is organized. Following a well-trodden pattern by accident usually means it’s the right pattern.
- Continuity-of-intent is the lighthouse feature. The cron stuff and janitor are useful; “you asked for v5, here’s the bench result waiting when you wake up” is the line that distinguishes a local-first sovereign assistant from a cloud chatbot. Worth designing the orchestration primitives with that endpoint in mind even if it ships last.
- Same protocol covers productivity AND defense. Watchtower doesn’t only schedule the janitor; it can also be the active security monitor for prompt-injection patterns, supply-chain drift, ollama outbound calls. The thing that schedules your janitor is the same thing that watches for prompt injections. That symmetry is unusual and good.
- Universal protocol > werbos-only protocol. Naming werbos anywhere in observe or watchtower would have permanently coupled three siblings to one consumer. Pulling werbos out of the spec vocabulary preserves the option for a future tool to use the same primitives without dragging in the agent runtime.
- Hosted-SPA + local-watchtower is the right deploy story. It’s how Plausible, Cal.com, etc. work — a hosted UI for convenience, user’s instance lives wherever they want. CSP-lock the SPA so even a hosted build can’t phone home, and you’ve got a privacy story that’s airtight by construction.
- No building tonight was the load-bearing constraint. The user’s directive — “smallest, most secure, timeless, protocol we can make” — would have been compromised by rushing to a runtime. The Archaeologist review caught three real blockers (ULID, shell allow-list, canonical JSON) that would have been wired into actual code before being noticed.
Quote of the day
“Smallest, most secure, timeless, protocol we can make.”
The design directive that produced everything else. Every spec choice in observe and watchtower traces back to it: HTTP/1.1 + JSON because curl-and-jq survives forever; UUIDv7 because RFC; RFC 8785 because cross-runtime hash agreement; argv-prefix shell allow-list because softer matchers always lose; CSP-locked SPA because the threat model has to survive hosted deploys.
The bar wasn’t “good.” The bar was “still-implementable in 2050.”
What’s next
- v5 specialist LoRAs on 3B-base (debate, fun-fact, scripture).
- Multi-model vision+tools routing in BellmanBackend
(
vision_modelconfig; vision model fires only when an image is attached, withtools=[]). - CLI streaming parity for
werbos chat. - The first orchestration primitive —
SessionRegistry.is_foreground_busy(). Smallest first step that unlocks everything else in the watchtower roadmap. gh repo createfor observe, watchtower, watchtower-demo when the SPECs feel locked enough to publish.- A
npm run apk:shipscript wrapping thetauri android build → bundletool extract → cp to APK_DROPOFFpipeline into one command. Manual ceremony every time we want to push a build to the phone has gotten old fast.