The fork (one runtime question)
frame-src allows our collar iframe? →
Axis A · bookmarklet.exit. Blocked? →
Axis B · bookmarklet.stream (host_base render + pixel stream + input replay).
No silent fallback — you pick (or a CSP probe hints).botskit/service_api at 127.0.0.1:8800 — this server roots at
caprpc/, so every link on this page (and in the map) resolves. The Axis-B-only server
serve.py :8123 roots at stream_frames/ and can't serve this page.Axis AB — Combined (one drag, both modes)
Tries A's iframe collar first; falls back to B's pop-around streamer on hostile-CSP hosts. Build-stitched from the two single-axis sources — both bodies live in full inside one IIFE, so the audit surface is just "A + B + a small orchestration shim".
What you trust
build_books.py to combine them faithfully. Hub URLs are baked
at sync time — the file is self-contained.What you experience
frame-src CSP violation OR a 1700 ms handshake timeout triggers fallback.build_books.py sync from
bookmarklet.exit.src.js + bookmarklet.poparound_xo.src.js.
DO NOT hand-edit the stitched file — edit the canonical sources and re-run sync.Read the stitched trust anchor
loading…
Axis A — Exit (in-page)
Fires when the host page's CSP lets our collar iframe load. Native paint, no fidelity gap. This one reached a full release + deploy chain.
What you trust
What you experience
Read the trust anchor (the whole surface)
loading…
Axis B — Stream (host_base + pixels)
Fires when CSP blocks our iframe. We render the real collar+widget in a host_base the page can't gate, stream PNG frames to a canvas overlay, and replay your input. The widget you see is pixels; the trust still lives in this one anchor.
What you trust
What you experience
chat.core.js lives
in axis-B's release (no _core/ shared dir; build_books.py would sync the
other way if A ever needed it). Rendered in the host_base and streamed back; read-page
rides bc.v0; the LLM loads only when you Send (idle = zero egress).router object, substituted by build_books.py from
build_books.toml.🧪 bridge experiments (host-only, not in trust anchor)
window.open() for OAuth, the host_base lacks user activation (synthetic
clicks from {t:'inject'} don't grant it) and the popup is blocked. The fix is
a relay: host_base's window.open is patched to forward to the HOST page,
which still has transient activation from the recent canvas click.bridge experiment idle.
⚡ Frame rate & liveness A1 shipped in anchor
The stream runs at ~8 fps (tick 120 ms) while the host_base window is
visible on screen. If you minimize or fully cover it, the browser throttles
its setTimeout to ~1 fps — frames crawl. That's a browser policy for
hidden pages, not our rate.
Every option to fight it, by family — the only question that matters is the middle column:
| option | back to ~8 fps? | status |
|---|---|---|
A1 — foreground page drives a postMessage tick (capture on
message receipt, not the hidden timer) |
YES | shipped in anchor |
| A2 — server / WebSocket pushes the tick | YES | opt-in (needs server) |
| B — in-page tricks (MessageChannel self-loop, Web Worker timer, AudioContext / muted video keepalive) | partial / no | softens floor only |
| Service Worker as a clock | NO | debunked — event-driven, killed ~30 s idle, no DOM |
| C — WebRTC peer / server render (browser-to-browser; nothing local paints) | YES | escape hatch (surfaced) |
| D — just keep the window visible / PiP | YES | ship today |
Shipped: A1 now lives in the anchor — it drives a foreground
{t:'tick'} each animation frame, so the stream holds ~8 fps even when the
host_base is minimized. Proven by test_external_tick_beats_clamp and a manual
minimized run (1→8 fps). Users re-drag once to pick it up. Full analysis + the AudioContext
myth-buster + the rasterization gate: stream_frames/docs/dev_frame_liveness.md.
Read the trust anchor (the whole surface)
loading…
Optimizations — tune Axis B (a click away)
dev_*.md.
Full map: readme_unified_bookmark.md.⚡ Frame rate & liveness solved (A1)
Background tabs throttle setTimeout to ~1 fps. A foreground tick drives capture
so the stream holds ~8 fps even minimized.
🔍 Stream quality / sharpness scale knob open
Renderer is settled (SVG-fo + lossless PNG). The blur is a devicePixelRatio issue — DPR only ever lost over the video encoder, never over PNG. This re-tests it.