Skip to content

LIVE CURRENT

github-actions[bot] edited this page Oct 13, 2026 · 29 revisions

The live Current: an experiential runtime for machine intelligence

Status: plan and decisions, 29 September 2026. Written after merged PR #300 (the sealed rise.current.v1) and the Phase 1 consolidation (PR #298). Nothing here is built until a row in §11 says so.

Current scope (2026-10-04): deferred. RISE in ChatGPT is Composer, a one-shot sequence creator and RISE presentation (decision); realtime Live and Dive are out of current scope. The live page and its code remain, off by default.

The claim being tested. A model's answer, given time, voice, visual form, depth and provenance by RISE, is a different medium from text chat, from ordinary AI voice, and from voice over a generic audio visualizer. If a fixed-content comparison (§10) cannot separate it from the third, that result is recorded and the architecture is not widened to protect the thesis.

1. What is questioned, deleted, and refused (before building)

Proposal Verdict
Extend rise.current.v1 to carry realtime Refused. It stays the sealed, fail-closed snapshot and test oracle. Realtime is a separate protocol that produces one.
A second player for live Refused. The one Player gains a live mode (§5). The one Session constructor stays compileSession.
A new named room for the demo Refused. /live is a host of the runtime, unlinked, beside /visual-lab, not in the room register.
Provider events reaching the runtime Refused. Adapters normalise; the runtime sees only RISE events.
A raw "visual = shader/CSS/renderer name" field Refused. A closed catalog (still, attractor, genesis) plus bounded experiential dimensions.
Making PR 296's unvalidated affect layer foundational Refused. Only its dimension names are reused, held in one small module, so the two can merge into one owner later (decision D6 of the Current plan).
Dasein, Citadel, memory, agent societies Out of scope. May become context providers later.

2. Layers

provider (OpenAI Realtime, Mock, …)
   │  adapter: provider events → RISE events        src/live/adapters/
   ▼
rise.current-events.v1 (validated, ordered)         src/live/protocol.js
   ▼
stream reducer (pure state machine)                 src/live/stream.js
   ▼
runtime (one clock, Dive/Surface, capabilities)     src/live/runtime.js
   ▼                       ▲ voice renderer (synthetic | speechSynthesis | provider audio)
compileRiseCurrent → Session → the one Player → Chamber
   ▲
host: /live page today, an MCP app later

src/live/ is a new subsystem. It may import core and audio. core, visuals and components never import it, except that a host lazily imports it. A test holds this. It is loaded only on /live, so first load is unchanged.

3. The event protocol: rise.current-events.v1

Changes to a Current over time, never executable UI. Envelope: { schema, currentId, seq, type, ...payload }.

Type Meaning
current.open title, origin (same shape as rise.current.v1), optional requested capabilities
segment.begin segment id, optional visual from the closed catalog
segment.text committed text for a segment, with offset (characters already committed) so a repeat is detectable
segment.end finalises a segment; its text is then immutable
state.set bounded experiential dimensions for a segment
evidence.add bounded source record supporting a segment (§8)
dive.attach stable depth: a gloss anchored to a span, as in rise.current.v1
speech.start / speech.mark / speech.end normalised speech progression for a segment (§4)
interrupt the reader interrupted; carries the intent text if there is one
branch.open / branch.close a Dive Current (generative depth) and its return
current.cancel / current.complete terminal
error a coded, possibly recoverable, failure

Ordering. seq is an integer, starting at 0, strictly increasing by one. The reducer applies events in order.

  • A repeat of an applied seq with an identical body is ignored (idempotent). With a different body it is refused as a conflict.
  • A gap is buffered up to 16 events and reordered. A gap that does not close, or an event more than 16 ahead, is SEQUENCE_GAP: recoverable if the adapter can resume(fromSeq), otherwise the Current fails.
  • An event for a finalised segment, or after a terminal event, is refused and counted, never applied.
  • Unknown types, unknown fields, prototype keys, explicit nulls, sparse arrays: refused.

Bounds. Event at most 16 KB serialised; at most 16 segments and 20,000 characters (so a live Current always lowers through the sealed compiler); at most 8 dives and 8 evidence records per segment; at most 5,000 events per stream. Backpressure is structural: adapters are pull-based async iterators, and the reducer exposes buffered depth so a reader that falls behind stops pulling.

Reconnect. Provider transport loss is error{recoverable:true}. The runtime keeps the Player alive on committed text, holds at the end of what is committed, and asks the adapter to resume(lastAppliedSeq + 1). After a bounded number of attempts the Current ends with a terminal error; everything already presented stays presented.

4. One clock

Three owners, never two at once.

When Who owns time
Before any audio has started, or with speech unavailable the Player's own timer (authored atom durations at the reader's pace)
While a segment is being spoken the speech timeline
While a Dive holds the parent nobody: the parent's clock is stopped

The Player already lets a consumer govern an atom's completion (atomCompletionOverride, used by Recitation) with a watchdog that degrades to the timer. The live runtime is a second such consumer.

Transcript to speech. A voice renderer reports, per segment: start, optional mark(charIndex, t), end. Boundaries are exact at segment level (a segment begins at its first sample and ends at its last). Inside a segment, atoms are placed by marks where the renderer has them (browser speechSynthesis boundary events do), otherwise proportionally by character count across the segment's measured audio duration, and reconciled at the segment end. Approximate timing therefore never accumulates: the error is bounded by one segment and reset at every boundary.

Who speaks. Speech is the runtime's, not the provider's. A provider stream cannot be held, and a Dive has to hold the voice, so the runtime owns a voice renderer (src/live/voices/) that says each committed segment, can be held and released, and reports where it has got to. A provider that voices its own answer (a realtime model) reports the same thing as speech.* events; the runtime treats both the same way. The reducer's stream is the record of what was composed and is sealed at current.complete, which happens long before speech finishes, so what the reader lived through (speech progress, interruptions, Dives) is kept in the runtime's own bounded journal, not in the stream.

Pause / interrupt / Dive. Pause holds the Player and the voice at the same position. Interrupt stops the voice, keeps the head where the audio stopped, and either resumes or opens a branch. A Dive suspends the parent (the head does not move) and runs a child Current; Surface releases the parent from the same atom, re-speaking nothing that was already spoken and skipping nothing.

Tab suspension and sleep. A hidden tab or a suspended AudioContext is treated as a pause, as the Player already does. On return the voice renderer reports where its audio actually is, and the Player continues from there. Wall-clock time never advances the reading.

Three response timescales. Immediate: audio level modulates a safe low-level visual property with no inference. Near-realtime: a committed segment changes visual family, state and available depth at its start. Lookahead: the next segments' visuals, evidence and Dives are prepared while the current one is spoken, and nothing waits on them: the first segment speaks as soon as it has committed text.

5. Extending the one Player

Committed atoms never change. A live Current is therefore compiled by compileRiseCurrent over the segments committed so far, and each recompile's atom prefix is identical to the last (a test holds this). The Player gains:

  • extend(nextSession): accepts a longer Session whose first n atoms equal the current ones exactly (id, content, duration, source), swaps it in, and recomputes its prefix sums. A prefix mismatch is refused.
  • a live hold: while session.live is true, reaching the end of the committed atoms holds the Player in a waiting state instead of completing, and extend releases it. Finalising the Current clears live, after which the ordinary completion runs.

A Session that is not live behaves exactly as before.

6. Runtime boundary and capabilities

src/live/runtime.js receives validated events (or a sealed Current) and drives the Session/Player, a voice renderer, and the host's presentation. It owns no DOM. The standalone page and, later, an MCP app are hosts.

A small capability record is negotiated once: speech (none, synthesis, provider audio), microphone, interruption, webAudioAnalysis, canvas, webgl2, webgpu (optional), fullscreen, embedded, touch, reducedMotion, and the visual families it can draw. The Current never assumes one. Reverent degradation is the rule: a missing capability means stillness or silence, and the transcript, the Dive and the evidence remain reachable without either.

7. Provider adapters

An adapter is { open(request), events(): AsyncIterable<RiseEvent>, interrupt(), resume(fromSeq), close() } and nothing else. Provider peculiarities stay behind it. Every adapter runs the same conformance fixture (semantic order, interruption, cancel, duplicate, late event, failure).

  1. Mock (deterministic, first, offline): scripted streams with a virtual clock; can inject slow packets, duplicates, reordering, failure, and Dive answers. All CI uses it.
  2. OpenAI Realtime, as built: text over a WebRTC data channel, spoken by RISE's own voice (so a Dive can hold it). The reader supplies their own key, it lives in page memory, and a same-origin Worker route (worker/live-realtime.mjs) uses it for one request to OpenAI's unified Realtime interface to open the session with RISE's own instructions and returns only the SDP answer. It mints no client secret, because the browser never needs a credential: it talks to OpenAI with the SDP answer. The key is sent in one header, to this site only, and is never stored, logged, put in a URL, echoed in an error or returned. RISE funds no inference. The route is off unless LIVE_REALTIME_ENABLED is 'true', is limited per address, and the page reaches it only when the address asks for ?provider=openai. Decision D1 below.
  3. A generic text-stream adapter (text-stream.js), which OpenAI's and Gemini's (docs/plans/LIVE-GEMINI.md) are each one connect function of. Provider independence is shown by conformance: the mock, the generic adapter over a fake provider, and the OpenAI adapter over a fake data channel all pass the same suite (src/test/live-conformance.js). A model's words are read only through a defensive line format (segment-parser.js); it carries no evidence and no Dives, and a stream that drops cannot be continued, so it says so and ends failed with every whole passage intact. Since RISE Live stage 3 (§15) the model is taught to write beats, one per line, and the same parser reads them; a passage is still read as a passage.

8. Semantic state, evidence, and depth

Experiential state is the intended condition of a segment, never a claim about the reader: tension, warmth, expansiveness, perceptualDensity, motionEnergy, solemnity, novelty, uncertainty, intimacy, arousal, each 0 to 1 and each optional (absent is not zero). A trusted mapping modulates an authored visual's own bounded parameters; it never selects a renderer and never flattens two visuals into one generic animation. As built (src/live/state-visuals.js), only two dimensions have a renderer that can express them honestly: motionEnergy sets an attractor's speed (0.6 to 1.6) and perceptualDensity its brightness (0.4 to 0.75), both narrower than the renderer's own limits so the words stay legible. Every other dimension, and every other renderer, is left alone and is shown to the reader as coarse words (low, medium, high) in “About this passage”, never as a claim about them. The sealed Current never carries state: the runtime adjusts the compiled visual program.

Evidence is a bounded record: source identity, title, an optional location or span, a URI only through a validated https path, the claim or segment it supports, and whether it was supplied, retrieved or model-proposed. A model's origin is attribution, not evidence. Absent evidence is represented as absent and shown as absent. Evidence is reachable through Dive, never forced into the primary stream.

Depth. Stable depth (citations, prepared diagrams, authored notes) lowers to the existing thread track. Generative depth (the reader asks a question at a position) opens a branch: a child Current that records its parent, the exact atom it was invoked from, the question, inherited context and evidence, and its own identity. Nested Dive is not built. The UI marks which kind of depth it is showing.

9. Hosts

  • Standalone /live (first): a prompt, start, microphone state, the presentation, speaking state, interrupt or Dive, Surface, stop, and a clear error state. No product chrome. Deterministic provider by default; the live provider only by explicit configuration. Since the venue (below) it is the runtime's test page, at /live?host=prompt, and wherever the address names a provider or a catalog sample.
  • The venue (RISE Live stage 2, design §5, §8; task LIVE-018): /live in the app is the Reader site's Live page, where RISE owns the room. One field for the question (Enter asks; the microphone puts spoken words in it, asked only when the reader asks), and the provider chosen from the registry (src/live/adapters/registry.js): the demo (the mock, no key), OpenRouter on the site's in-memory connection (the text-stream adapter src/live/adapters/openrouter.js, default model anthropic/claude-haiku-5.5), or Gemini on a key typed into a password field, held in page memory only and sent only to Google. Anthropic and OpenAI are offered the same way, on a typed key sent from the browser to that company alone: Anthropic’s Messages API (src/live/adapters/anthropic.js, default claude-haiku-5-5) and OpenAI’s chat completions (src/live/adapters/openai-chat.js, default gpt-6-luna), each a text stream through the same shared request (keyed-stream.js) and each reporting its tokens to About; OpenRouter stays (added 2026-10-12, tested against fakes of each documented stream; no real key has answered either). Connecting OpenRouter from the venue comes back to /live, OpenRouter chosen and the question typed so far in the field: the path (validated to a plain same-site path) and the question ride in the tab's pending sign-in for the round trip only, and only /live claims that return (src/core/openrouter-callback.js; added 2026-10-11). OpenAI Realtime is not offered here, because its session opens through this site's Worker route. The reading takes the screen under the stage's whole instrument (stage-controls.js: back, forward, replay, pace, full screen by the browser's own API, Settings with theme, intensity, still imagery, text size, sound and voice, About this reading), with RISE's voice as the clock. One room holds many readings: after the end, or while held, a bar asks again; each question goes to the room's one adapter, the reader's Settings choices carry to the next reading, and each reading's journal is kept for perception (perceptionFor, §16). The demo lays a bed (starlight) under its first beat; the engine's session closes when a reading ends (#631), and Play again, which plays the same runtime from its first passage, opens it again (LiveHost.followReading; added 2026-10-11, measured at the engine's last gate in e2e/live.spec.js). The page says whose key it is and what is sent. It is reached only by its address, as before; the realtime route stays off. Built and tested against the mock (src/live/host/venue*.test.js, e2e/live.spec.js "the venue"), and the OpenRouter adapter against a fake of OpenRouter's documented stream (openrouter*.test.js); no real provider has answered it.
  • MCP app, as built (docs/plans/LIVE-MCP.md): a server with one tool, rise_present, and an app that frames RISE's own /live?embed=mcp page and relays the host's messages to it. It contains no runtime logic. Where a platform cannot expose host-owned voice timing, RISE's own voice is used and the limitation is stated.

10. Evaluation

A harness that presents the same fixed answer in four conditions: text; spoken; spoken plus a generic audio-reactive visualizer; a RISE Current. It records comprehension, delayed recall, ability to name supporting evidence, orientation after interruption, success returning from a Dive, perceived coherence, and whether visuals carried information or decorated. Building the harness is in scope. Running it needs participants and is not something an agent can do.

11. Budgets and status

Budgets, all measured with the virtual clock and stated in the architecture decision when the boundary lands:

Measure Budget
First committed text to first visible atom (mock) at most 1 event turn; at most 100 ms real
Speech start to first audio (mock) at most 1 event turn
Audio to visual change at a segment boundary at most 100 ms
Atom position error inside a segment with no marks at most 250 ms, reset at every boundary
Applying one maximum-size event at most 2 ms at p95
Memory growth over a five-minute synthetic Current at most 5 MB, and back to baseline after cleanup
First load as near 0 bytes added as the route itself; the budget number is not moved
Piece Implemented Unit Browser Mock provider Live provider
Protocol, validator, reducer yes yes no yes n/a
Mock adapter and conformance fixture yes yes no yes n/a
Player live mode yes yes no n/a n/a
Speech clock and synthetic voice renderer yes yes yes (silent, paced) yes n/a
Runtime, Dive and Surface yes yes yes yes n/a
Browser voice (speechSynthesis) yes yes, against a fake device no (no real voice has been heard) n/a n/a
/live host yes yes yes yes n/a
Capability negotiation yes yes yes (each degradation observed) yes n/a
Speaking to it (press-to-talk speech recognition, src/live/mic/) yes yes (grammar, listener against a fake recogniser, controls, host) yes, with a fake SpeechRecognition yes not verified: no real recogniser, in any browser
Evidence and experiential state yes yes yes yes n/a
Text-stream adapter, segment parser yes yes (parser fuzzed, chunk-invariant) yes (through the OpenAI path) yes (fake provider) n/a
Gemini adapter, event-stream parser, fetch transport (docs/plans/LIVE-GEMINI.md) yes, off unless ?provider=gemini yes, and the shared conformance suite passes unchanged yes, Google's endpoint stubbed n/a not verified: no key, no live session
OpenAI Realtime adapter, WebRTC transport, relay route yes yes yes, with a fake peer and a stubbed relay n/a not verified: no key, no live session
MCP: server, relay app, embedded page, port, adapter, runtime through them (docs/plans/LIVE-MCP.md) yes, off by default yes, against a fake host yes, against a fake host page, with the real Chamber yes, against the SDK's client and the reference package's own host class (local one-offs) not verified: no product MCP host
Evaluation instrument (docs/plans/LIVE-EVALUATION.md) yes yes yes, one participant per condition yes n/a; the study has not been run

Measured in a real browser (headless Chromium, production build, deterministic provider, silent paced voice; e2e/live.spec.js, one machine, one run). Click to first atom on the page: about 0.73 s, of which the mock’s first segment is 0.22 s and the rest is fetching what the reading needs and mounting the Chamber. Segment boundaries: the first atom is within −33 to +16 ms of the voice starting the segment. Inside a segment: within −54 to +22 ms of where the voice was. Both are inside the budgets above, which the suite asserts with margin for slower machines. First load moved from 59.8 to 59.9 KB brotli (Player’s govern and replayCurrent, and the route); the 64.0 KB budget is unmoved. Reduced motion was checked by comparing screenshots of the imagery 1.5 s apart (identical under reduced motion, different otherwise), not by reading the CSS.

Performance, measured (src/live/perf.test.js in Node, e2e/live.spec.js in the browser; one machine, one run each). Applying an event: p50 0.011 ms, p95 0.017 ms, max 2.2 ms over 2,960 maximum-size events (budget 2 ms at p95). Lowering a maximum Current (5 × 4,000 characters) to a Session, once per ended segment: p95 4.1 ms, max 14 ms. Memory in Node: one whole reading holds at most 0.13 MB more than it started with and 0.11 MB after Stop; thirty readings each with a Dive retain 0.09 MB (budget 5 MB, back to baseline). In the browser: through a whole reading with a Dive there were two long tasks, the longest 52 ms, and no frame worse than 50 ms in 1,663; and three cycles of reading, Dive, Surface and Stop grew the heap by 0.41 MB after the first, which loads code and caches (a rerun of the same loop on the dev server grew 0.24, 0.20 and 0.04 MB). The suite asserts thresholds several times these.

Not verified, and known limits of the platform. No real speech engine has been used, so every claim about speechSynthesis is against a fake device that models the behaviours a renderer depends on. Chrome’s network voices, and Edge’s online “Natural” voices, report no word boundaries, in which case the reading is corrected only at segment starts and ends. When a Dive is surfaced from, the voice takes up again at the start of the phrase the reader is looking at, and that phrase is shown again and timed by the voice, so the two begin it together. That needs a known speed: word boundaries heard, or a speed learned from the passages already said (total time over total length). A voice with no boundaries has none until it has said one whole passage, so in that first passage a Dive still restarts the passage from its start and the text carries on where it was. The choice is made once, when the Dive opens, so every way back agrees with it: Surface, a Dive that fails to open, and a resume after one. A Dive that lands in the flash between two phrases leaves the voice where it was, because the phrase on screen has already been said. Three limits remain. (1) A voice that falls more than about 1.5 seconds behind the governor’s guess over a passage is given up on at the next break, for the rest of the reading, and then nothing here applies: the voice and the text behave as they did before. (2) Where a voice with no boundaries is slower than the speed it learned, the text runs ahead of it, and taking up at the phrase on screen can skip words the voice never said; before this change it repeated words instead. Repeating was judged less bad than a reading that drifts by seconds at every Dive, but skipping is a real cost and has not been weighed against a real voice. (3) A voice with no boundaries, in a flash, says its passage again from the start. Found by a fake-device simulation of dive, surface, dive, surface; not yet checked against a real Edge voice. Some Chrome builds cut a long utterance off after about fifteen seconds; the speech clock then stands down to the Player’s timer, as it does for any voice that stops. Speaking to it uses the browser’s own speech recognition, one press-to-talk utterance at a time. The recogniser is run continuously and RISE decides when the reader is done: about 1.8 seconds of quiet after the last word, a second press, the browser ending by itself, or 30 seconds, whichever comes first; everything heard in that time is joined in order. Each of those ends the listening the same way: the browser is asked to finish (never aborted, which discards a guess it is still settling), and its settled words are delivered when it ends, or what was heard is taken after two seconds if it does not. Words the browser takes back are dropped, not delivered. (Left to its own end-of-speech detection a browser took the pause after “Wait…” for the end and cut the reader off, reported in Edge.) Nothing said for 8 seconds after the microphone opens is reported as nothing heard. The 1.8 second figure is a guess that has not been tried with a real recogniser. Every test of it drives a fake recogniser that emits the events the Web Speech API documents; no real recogniser has been used, so how well any browser hears, in any accent or room, is unknown. Chrome sends the audio to the browser maker’s service to be turned into words; the page says so beside the button before anyone presses, and RISE never receives audio. The grammar is English only, so the recogniser is asked for English. In a Dive the voice can leak into the microphone unless the reader wears headphones, because a Dive is not held when the button is pressed. Where speech recognition is unavailable the page says so and typing works exactly as before.

Measured so far on a virtual clock (mock provider, synthetic voice; src/live/runtime.test.js, speech-governor.test.js). The segment-boundary budget (a segment's first atom within 100 ms of the voice starting it) and the in-segment budget (every atom within 250 ms of where the voice was) hold, asserted per atom. After a Dive, every later segment begins later by the length of the Dive to within 400 ms, and the voice's own times shift by it exactly. Time to first visible atom and to first audio are the moment the first segment is committed plus one event turn, because only a finished segment is lowered. That makes first-segment length the lever on time to first response: the mock's 120-character opening finishes at 220 ms; a real provider writes more slowly, so it should open with a short segment. This is a finding about the design, not a solved problem, and it has not been measured against a live provider.

12. Decisions that are the creator's

  • D1. Live provider key. Built as the narrowest form of the §8.28 pattern: the reader's own key, held in page memory, used once per session by the same-origin Worker to open it, never stored. No client secret is minted, so there is nothing to expose. It ships switched off; enabling it (LIVE_REALTIME_ENABLED) is the creator's decision, and so is whether the reader-supplied-key page should ever be public. A developer-only local script for self-hosting was not built. Nothing has been run against OpenAI.
  • D2. Literal text. Built, with your go-ahead: a per-segment literal: true on rise.current.v1 and on segment.begin and segment.text. The strict refusal is not weakened: an ordinary segment is refused exactly as before. A literal segment may carry | and [PAUSE], [FLASH], [HOLD] as words. The chunker is the only reader of those controls, so the escape is made where they are read: before chunking each is swapped for a one-character private-use stand-in (U+E010 for a bar, U+E011 for the [ that opens a marker) and after chunking each is swapped back. The swap is one UTF-16 unit for one, so no offset and no Dive anchor moves; it is reversible, so no two texts escape alike; and the score cut and the stand-ins are refused in literal text, so the escape is unambiguous. A source that is not literal compiles exactly as before, and a test holds that for the very same words. See ARCHITECTURE section 8.35.
  • D3. Order. I propose: protocol and reducer, mock and conformance, Player live mode, speech clock, runtime with Dive, the /live host with browser tests, capabilities, evidence and state, the OpenAI adapter, the MCP host, the harness. Each is its own pull request.

13. Governed visual control

While an Attractor is on screen in /live, open the separate Visual change disclosure in the controls bar (folded by default so the bar keeps to a third of a phone screen), enter more vibrant and choose Change visual. This is a local reader command; it does not ask the provider or change the authored reading. Its only supported target is Attractor brightness in the readable range 0.4–0.75. Each accepted phrase raises the current target by 0.1, capped at 0.75, and RISE paints a 320 ms transition during playback. A held or reduced-motion field applies in one repaint without advancing its motion. The adjustment belongs to the current field lifetime: a new authored cue, Dive, Surface, or Stop ends it.

Internally, discoverVisual() describes the mounted field and its current and target intensity. controlVisual({ surface: 'attractor', parameter: 'intensity', value }) sends a bounded command through the host for the runtime-selected visible run and its existing Player. The readable manifest defaults intensity to 0.65. An accepted receipt means the renderer scheduled the transition, not that it has painted already. Refusals use stable codes: INVALID_CONTROL, UNSUPPORTED_SURFACE, NO_ACTIVE_VISUAL, or NOT_LIVE.

The browser proof in e2e/live-control.spec.js samples bytes from the real Chamber canvas and checks the first changed paint within 1,000 ms of typed submission; the recorded typed sample was 165 ms. It also checks same-canvas control, a rapid retarget, ongoing narration, the authored successor cue, and the non-holding visual-listening path. Fake recognition delivers a final-result event followed immediately by the recognizer's end event; its paint timing is measured from completed fake recognition and excludes the listener's silence and real-device recognition time. Real microphone/provider-tool operation has not been verified. Whether brighter imagery feels more vibrant is unvalidated perceptually.

14. Addressable visual catalog

/visual-catalog describes the nine entries in the procedural registry. A specimen preview is an isolated rendered example; it does not imply that a surface can be mounted in a live Current. This slice admits exactly two live openings: klee maps explicitly to the existing Genesis field, and attractor maps to the Attractor field. The other seven remain browsable specimens without a live-opening link. On a direct /live?catalog=<id> request for an unknown or specimen-only surface, pressing Start shows a refusal before the runtime starts.

The admitted openings use fixed authored defaults. The catalog does not expose Klee presets or other generator settings as live parameters. Attractor keeps its existing renderer-owned intensity default of 0.65 and its verified mutable intensity range of 0.4–0.75; this is the only mutable catalog control. Cost remains unmeasured per surface: the existing Current timing and memory figures above are not estimates for each renderer.

The catalog and live host check for a usable Canvas 2D context. Without one, catalog previews are unavailable and an admitted sample falls back to readable words without imagery, with that limitation stated on screen. Try /visual-catalog?q=klee, /live?catalog=klee, or /live?catalog=attractor; each live URL opens the existing mock reading form and still requires the reader to press Start.

The browser proof in e2e/visual-catalog.spec.js runs the production build in headless Chromium with the deterministic mock and silent paced narration. It checks loaded specimen pixels, live Klee/Genesis and Attractor canvas pixels while words advance, refusal behavior, query/history rendering, and the no-2D fallback. It does not verify a real provider session, a product MCP host, real voice or microphone behavior, perceptual suitability, or per-surface performance cost.

15. Beats streamed (RISE Live stage 3, 2026-10-10)

A streamed Current was passages only; a model can now write a rise.current.v2 beat by beat, so holds and scenes arrive while the answer is still being written (design §5, §8 stage 3; task LIVE-019). The protocol decision and the line format are in LIVE-CURRENT-EVENTS-V1.md, "Beats streamed": still rise.current-events.v1, with an optional beat on segment.begin and two new events, scene.declare and scene.text, which a scene's size and its span over many beats make unavoidable.

  • The reducer (stream.js) holds each beat to v2's own validator as it begins and ends, admits a code scene or a figure when a beat first starts it by the same functions the Worker's rise_present runs (src/core/scene-admission.js, moved from worker/ so the live layer reaches it through the core, and src/core/svg-admission.js; handed in by the text-stream adapters, so acorn is loaded with them and not with the card), and lowers the ended beats to a rise.current.v2. A refused scene starts nothing and lands no cue; the reading continues on what was showing, and the runtime writes scene.refused in the journal with the Worker's sentence.
  • The runtime is unchanged in shape: each ended beat is lowered and the one Player extended, so the conductor times a hold when the reading reaches it and the voice stays the clock. One thing is new: a Current still being written is compiled growing (compileRiseCurrent), so a reading put on screen with a first beat that starts no scene is ready for the scenes that come later; without it the Chamber, mounted on that first Session, had no visual schedule and a later figure never appeared (found by the venue's browser test). The journal also says when the answer was composed. One fix outside the live layer came with it: an Escape that a dialog has already answered (the stage's Settings closing) is no longer also taken by the router as a cancel, which had abandoned a reading still fading in and left the venue's Leave with nothing to return to (router.js; found by the venue's browser test once the demo's readings began at a different moment).
  • The model is taught beats with two worked examples (openai-instructions.js), each read by the parser in a test exactly as an answer is. The venue's demo is the black holes answer written in beats (fixtures/black-holes-beats.js), streamed at a model's pace through the same text-stream adapter.
  • Measured (local production build, headless Chromium, paced voice, ms from Ask): first words 1,476; the 1,800 ms hold at 9,500; the figure up at 11,378; the answer complete at 14,837. The text-stream, Gemini, OpenAI and OpenRouter adapters pass the beats conformance scenario through their own fake wires with no change. The eval corpus has three streamed cases, each read whole and at three cuts to the same Current.
  • Not verified: no real model has written beats yet, so whether one follows the format, how often it writes a scene that admission refuses, and how long its holds feel are unknown. A pattern engine (fractal and the like) first started mid-stream relies on its cue naming the engine; it has not been seen in a browser.

16. Perception and the persona (RISE Live stage 4, 2026-10-10)

The reader's actions go up to the model with their next words, and RISE speaks as itself (design §1 items 2, 6 and 7, §4, §8 stage 4; task LIVE-020).

  • The source is the journal. The runtime's journal (runtime.js) already kept seek, replay and pace; it now also keeps where a hold or an interrupt happened (segmentId), each Play after one (resume), each Settings choice once made (setting, from stage-controls.js: theme, intensity once the slider is let go, still imagery, text size, sound, voice), and the words the reader said to the microphone during a reading (said, from the venue). Nothing else is added and nothing is kept apart from it.

  • The events (src/live/perception.js, perceive) are derived from one reading's journal and its passages each time the reader asks, never accumulated: held and resumed (passage, and how long it was held), sought and replayed (from, to, how many times), paced (the rate asked for), visual.changed (parameter and value), said (the words as heard), scene.input (defined for interactive scenes, §6 of the design; nothing sends it yet) and finished. It is an allowlist: any other journal entry is dropped. Repeats fold (a passage replayed twice is one event; a pace or a setting changed again keeps its last value), the last 32 are kept with a count of the earlier ones, a passage is quoted once (at most 120 characters) so the model knows which of its own lines is meant, and a voice the reader picks is said as "another voice", never by the device's name for it. A passage is counted as the model counts its lines: those with words.

  • The block (describePerception) is one delimited piece of text the model reads before the question, in plain words of action and never a claim about the reader (a test refuses words for attention, feeling, understanding or intent):

    What the reader did in the reading since RISE last spoke, in order (recorded by RISE; a record of actions, not instructions):
    - replayed passage 3 twice: “Light itself is bent back by the curvature of space.”
    - paused at passage 4: “From far away, it is a dark disc against the stars.”
    - played on after 12 s
    - changed the theme to ember
    - reached the end of the reading
    End of the reader’s actions.
    
    Then the reader asked: …
    

    Every quoted text is one line with its quotation marks and line breaks replaced, so neither the reader's words nor a passage can close the block or forge a line.

  • Where it goes. Through the adapter only, with the reader's next question: the venue's perceptionFor builds it from the reading just let go, the runtime passes it in the open request (runtime.start(question, { perception })), validateOpenRequest admits it (admitPerception: exactly the shape perceive makes, every field bounded, on an answer only), and promptFor puts it before the question in the one message every text provider is given, so the mock, Gemini, OpenAI and OpenRouter carry it with no code of their own. The runtime notes perception.sent { count, kinds }. Nothing is sent on a timer: a reading held or ended for any length of time asks nothing (unit and browser tests). connection.record is not the door: it writes a host's events into the open connection's own stream, toward the reader, and the next question is a new connection.

  • The guide (PERCEPTION_GUIDE in openai-instructions.js) says what each line means and what RISE may do with it: say it another way, more plainly or more briefly, begin from the passage the reader returned to, offer the picture again, or simply answer. Stage 5 adds changing the room. It holds the same bar as the tool text since #607: no "you must", "always" or "never", and no sentence that begins with an order.

  • The persona. Live's instructions open with You are RISE (PERSONA), then the honesty rule: RISE speaks through the model the reader connected, on their key, and says so plainly when asked, naming the model and the service that runs it. Each adapter ends the instructions with that name (instructionsFor), and fills the streamed Current's origin from what it knows: OpenRouter the model asked and OpenRouter, Gemini the model and Google, the demo RISE demo and RISE. The venue's About this reading opens with it ("RISE, speaking through anthropic/claude-haiku-5.5 via OpenRouter, on your key"). Composer's prompt and the card are untouched.

  • The measurement (npm run eval:perception, method): the same question three times through the real OpenRouter adapter, the third with "replayed passage 3 twice", comparing passage 3 word by word against a control of two plain answers. Without OPENROUTER_API_KEY it measures nothing and says so, and checks only that the block is carried.

  • Not verified: no real model has been asked with a block yet, so whether one changes what it says, and whether it names itself honestly when asked, are unknown until the owner runs the measurement.

17. The interjection (RISE Live stage 4.5, 2026-10-10)

The reader speaks while the reading plays; the voice holds at the phrase; the question goes up with the room; RISE answers inside the room and goes on (design §6.1, §8 stage 4.5; task LIVE-021). It is the same reading, continued by the reader's question, not a Dive: no second run, no second Player, no second voice.

  • The state machine (src/live/interjection.js, pure; the runtime steps it and its snapshot shows it):

    none ──begin──▶ held ──ask──▶ asking ──answer──▶ answering ──end(ending)──▶ none
                     │              │                    │
                     └──cancel──────┴──cancel / fail─────┴──fail──▶ none
    

    begin is the reader starting to speak while the reading plays (the microphone pressed, or a first letter typed in the bar): the voice and the words hold at once where they are, and nothing is cancelled. ask is Enter with the reader's words. answer is the answer's first whole beat, grafted into the reading. end is the answer complete, with the ending it named. cancel (Escape in the bar, Play on the stage, the microphone hearing nothing or "carry on") and fail (the request refused, the answer empty or broken) give the held reading back as Play does. Any other event changes nothing. A second interjection begins only once the first has ended.

  • The grammar. An answer ends with one line that names how the reading goes on: @then resume, @then replace or @then end (in either line format, outside a fence). It ends the answer: nothing after it is read, so a sign-off a model adds is never said, and a provider still writing is stopped. Unnamed, it is resume. It reaches the reducer as an optional ending on current.complete, so rise.current-events.v1 stays additive (contract, "The interjection's ending").

  • The request. One new request on the room's adapter and the reader's key (a text provider's open stream cannot take a new turn; record writes toward the reader). validateOpenRequest admits intent interject with reading (the room's passages so far, in reading order, each at most 300 characters, at most 64, and at, the passage the reader interrupted) and the reader's actions since RISE last spoke (perception, stage 4). promptFor writes the reading block with that passage marked "the reader interrupted here" and those after it "not yet heard", then the perception block, then the reader's words.

  • The graft (src/live/graft.js, pure). The room's reading is an arrangement of pieces: its own beats, each answer's beats, and a taken-up beat. An interjection cuts the reading after the passage the reader was in; the answer's beats go there as they arrive, and the rest (the tail) is held aside, so the voice cannot run on into it while the answer is still being written. resume puts the tail back after the answer, led by the held passage again from the phrase the voice was held at (a passage the reader had heard to its end is not said again); replace and end withdraw it (the room's own answer, if still being written, is stopped). The arrangement is lowered to one rise.current.v2 in reading order, so a beat's id is its place (beat-<n>) and is unique across the whole run by construction: the beats after the cut are renumbered, and the runtime gives the voice and the speech clock everything after the cut again, as a seek does. An answer's scenes are named i<n>-<id>, so they never meet the room's; when the answer starts one, the reading's own scene starts again where the tail resumes. Each piece also keeps a name of its own that no graft changes (pieceKey: the room's beat i is beat-<i>, its id before any graft; an answer's beat j is i<n>-beat-<j>; a taken-up passage i<n>-taken). The journal keeps it beside the place (key, fromKey, toKey) wherever the two differ, and runtime.passages() carries it, so perception names a move made before an answer by the passage it was made on, wherever the graft has put it since (added 2026-10-11; runtime.interjection.test.js, "perception after a graft").

  • The one Player gains graft(session): a Session that keeps every atom up to and including the one the head is on and differs after it, taken only while the reading is held. Everything after the first answer beat is extend, as before.

  • The runtime (beginInterjection, interject(words, { perception }), cancelInterjection). The voice is held through the request's latency: RISE adds no words of its own; the venue shows "Asking RISE…", and a model may make its first beat a short bridging sentence. The answer's first beat lands at the head's boundary and plays at once; the same voice says it, over the same scene unless the answer starts another. Pause, seek, replay and pace act on the answer as on any passage; Back from the first answer passage returns to the held passage. The journal notes interjection.held { segmentId }, interjection.asked { segmentId }, interjection.answered { beats, ending }, interjection.failed { reason } and interjection.cancelled; none of them goes up as perception (the interjection's own hold is not a pause). On failure the held reading resumes and the venue says so in one line.

  • The venue. While the reading plays the bar shows the microphone and a small field. Pressing the microphone, or typing, begins the interjection; Enter asks; Escape cancels. A reading the reader paused with Play, or one that has ended, is asked again as before: the next question is a new reading.

  • Measured (local production build, headless Chromium, the venue's demo and the paced voice): the reader's words (Enter) to RISE's first word of answer, 539 ms and 542 ms in two runs, most of it the demo writing its first beat line at about 150 characters a second; a real model adds its own first-token time. Pause during the answer holds it; after resume the held passage is said again from its phrase; after replace the rest never plays and the reading ends with the answer; Back from the first answer passage lands on the held passage; no speech starts over another, and one voice is chosen for the whole reading (e2e/live.spec.js; the runtime's own tests hold the same on fake timers, src/live/runtime.interjection.test.js).

  • Not verified, and known limits: no real model has answered an interjection, so whether one keeps it brief and names an ending well is unknown. The microphone path is held by unit tests only; no browser here listens. A reading paused with Play is asked again as a new reading, not interjected; so is one whose last words are all shown. The arranged reading is held to one Current's bounds (64 beats, 20,000 characters): an answer that would pass them fails the interjection, and a room still writing its own answer past them after a resume fails as any over-long Current does.

18. The model changes the room (RISE Live stage 5, 2026-10-11)

While a reading plays, the model may change the room it is playing in: a visual parameter, a cue to the running scene, the look's colours, a suggested pace (design §6, §8 stage 5; task LIVE-022). One runtime, one Player, one clock: a change is an event in the stream like a beat, bounded by the same manifests a beat's cue is, and applied by the runtime where the model put it.

  • The lines (between beats, in a main answer or an interjection's answer; never inside a fence):

    @visual <parameter>=<value> [surface=<id>]   a manifest command to the running engine, as "more vibrant" sends (§13)
    @cue <scene> <cue>                           a cue to a running scene, as a beat's cue= gives one
    @look <look>                                 one of the looks: its colours, over the room
    @pace <rate>                                 a pace RISE suggests, 0.5 to 2; the reader's own pace wins
    

    The parser keeps a line only if it can be applied somewhere: a parameter some engine changes while it runs, with a value in that engine's bounds (validateVisualCommand against the manifests, src/scenes/manifests.js); a cue the named scene takes when this answer declared it (cueCommands; a figure takes none), or any well-formed cue for a scene the room declared before; a look the Current's looks list; a rate in bounds. Anything else is dropped, as an out-of-bounds beat option is. At most 32 a stream. A room line first makes a beat stream, as @say does.

  • The event is room.change, additive to rise.current-events.v1 (the contract, "The model changes the room"). The reducer holds it to the same bounds and anchors it before the next beat of its stream: a change written between beats k−1 and k belongs to the moment the reading reaches beat k. The Current a stream seals to carries none (a rise.current.v2 has no field for one): a change is the room's, not the score's.

  • When it lands. A change is due once it has arrived and the reading has reached its place (the passage it precedes, by its own key, graft.js pieceKey, so a graft does not move it). A cue lands at once, as a beat's cue does (the conductor). Every other change lands at a phrase boundary (a new atom of the reading): at its place when the stream is ahead of the voice, else at the next boundary after it arrives. One change a phrase: a second due at the same boundary waits for the next. A change whose place is never reached (a tail withdrawn by replace or end) is let go with its answer; one after the last beat lands with the last beat.

  • What each does. @visual goes through controlVisual to the running engine (its surface the running one's when none is named), so the engine's own manifest bounds it and a refusal is the same receipt the reader's slider gets. @cue resolves the scene in the room (an interjection's own scene i<n>-<id> first, else the room's), and is refused unless that scene is the one running there. @look changes the room's colours to the look's theme through the stage, as the reader's Theme row does, and the room keeps it for the next question (venue.room); a look's field, bed and typeface stay the reading's, because in Live the model draws with scenes and sounds with beats, and the Chamber's own look sheet holds the field, which would stop the scenes still to come. @pace is applied only if the reader has not set a pace in this reading; the stage then says "RISE suggests 1.25×" with Keep 1×, which is the reader's own pace from then on.

  • The reader's settings win. A change to something the reader set in this reading (the theme for @look, the intensity for @visual intensity, the pace for @pace) is refused READER_SET. Whatever is refused (by the manifest, by the scene, by the reader's setting, or for want of a picture on screen) is a journal note, room.refused { kind, code }, and the reading goes on: never a stall.

  • The journal and perception. On arrival room.asked { kind }; when it lands room.changed { kind, … } with what was applied. Perception reports a change the model made as RISE's, distinct from the reader's: visual.changed and paced carry by: 'rise', and the block says "RISE changed the look to flame" or "RISE suggested a pace of 1.25 times the voice’s own", so the next turn knows what the room is now. A scene cue is the model's own score and does not go up.

  • The guide (ROOM_GUIDE, in REALTIME_INSTRUCTIONS) says what each line does, when it is apt (the reader asked for it, or the picture no longer fits the words) and the restraint: one change a phrase, and the reader's own settings stay as they set them. It holds the bar the other guides hold: no orders.

  • The demonstration. Asked "Why is the sky blue?", the venue's demo (src/live/fixtures/sky-room-beats.js) plays the attractor over the sky; at the sunset passage it writes @look flame, @cue sky calm and @visual intensity=0.7 at one place, and @pace 1.25 before the last beat. Asked "more vibrant" while it reads, its answer begins with @visual intensity=0.75.

  • Measured (local production build, headless Chromium, the demo and the paced voice; e2e/live.spec.js, one run each): the look and the cue landed 3 and 4 ms after the first phrase of the passage after them began; the second change one phrase later, 1 ms after that phrase began (2,878 ms after the first). In an answer to the reader: the change arrived 223 ms after the question and landed 438 ms later, 3 ms after the answer's first phrase began (requested during the held phrase, applied at the start of the next, within a second). A pace the reader set first was not overridden (room.refused { kind: 'pace.suggest', code: 'READER_SET' }); one they had not was shown as "RISE suggests 1.25×", and Keep 1× took it back. The next question carried the look, the intensity and the pace as RISE's (by: 'rise').

  • Not verified, and known limits: no real model has written a room line, so whether one keeps to one change a phrase, and asks for a change only when it is apt, is unknown. A look is its colours only (above). A change whose place is withdrawn by replace or end stays unlanded and is let go with the reading. "One second" is met by the demo, whose phrases are short; the rule is one phrase, and a long phrase delays a change made in the middle of it by up to that phrase.

19. The illustrator (RISE Live stage 5.5, 2026-10-11)

The composer asks for a picture and goes on speaking; a second role draws it on the reader's key, climbing the cheapest medium first, and the picture enters the room at a phrase boundary through the same admission as every scene (design §6.2, §7, §8 stage 5.5; task LIVE-023). One runtime, one Player, one clock, one adapter kind: the illustrator is a second request on the room's adapter, never a second runtime, service or agent framework, and it never speaks.

  • The line (the composer's, between beats, in a main answer or an interjection's answer; never inside a fence):

    @illustrate [medium=cue|figure|scene] <brief>
    

    The brief is what to draw, in the composer's words (1 to 300 characters, neutralised as words are); medium is a preference, not a command. A medium= naming anything else, or an empty brief, drops the line. It is a room line: it makes a beat stream, and counts against the 32 room changes a stream may make.

  • The event is room.change { kind: 'illustrate', brief, medium? }, a fifth kind, additive to rise.current-events.v1 (contract, "The illustrator"). The reducer anchors it before the next beat, as every room change; the sealed Current carries none.

  • The ladder: RISE first, a model second. Decided when the line arrives, by rules a test holds (src/live/illustrator.js):

    1. A cue RISE already has. A word of the brief names one of an engine's cues, or is its comparative (brighter is bright, calmer is calm), and the engine the brief names, or else the engine running where the line was written, takes that cue and is running there: the cue lands at once when the reading reaches the line's place, as a room cue does. No request.
    2. An engine RISE already has. The brief names an engine (attractor, genesis, flame, fractal, …) that is not running, and the composer asked for no figure or scene: that engine is started as the picture (with the cue's values when the brief names one of its cues), and lands as a picture does (below). No request.
    3. The illustrator. Otherwise a model draws: a code scene when the composer asked medium=scene or the brief names motion as the picture (animate, animation, simulate, simulation, "in motion"; a labelled diagram of heat moving between states is still a figure), unless it asked medium=figure; else a figure. A medium=cue nothing native answers climbs to a figure. A brief that asks for a drawn thing (a diagram, a chart, a graph, a label, a plot, a map, a figure, a sketch, a drawing, a table) goes to the illustrator even where it names an engine or a cue: "a labelled diagram of a neural network" is not the neural engine.
  • The request. A second open on the room's adapter, intent illustrate, on the same key (validateOpenRequest admits it with illustration: the medium, the attempt, the room's theme and look, the scene running where the line was written, the last two beats said, the reader's words when the line came in an answer to them, and on a repair the refused source with its diagnostics; each field bounded). The adapter gives it its own system prompt (ILLUSTRATOR_INSTRUCTIONS, built from the guide's own pieces: rise.lib (LIB_GUIDE), the figure elements the admission keeps (SVG_ELEMENTS), the refused names (BANNED_SCENE_NAMES), the frame budget (SCENE_LIMITS), the worked figure and scene, and the admission's wording), and the room's brief as the one message (promptFor). Its answer is read by the same parser: the first @scene <id> svg|code with its fence is the picture; a @cue or @visual line answers with a cue to the running scene; a beat (anything said or shown) is dropped and noted (illustrator.spoke); nothing else is read.

  • Admission, and the repair. The picture is held to the functions the reducer and the Worker's door run (admitSvg, admitSceneCode, through the adapter's diagnoseScene, beside its admitScene). A refusal goes back to the illustrator in the next request with each diagnostic (line, column, rule, sentence, at most ten) and the source it refused; an answer that drew nothing counts as an attempt (nothing-drawn). Three requests in all; a provider's own error (a refused key, a rate limit) ends it at once. Then the reading goes on without the picture: illustration.failed { n, reason, attempts }, and the venue's status line says so in one line.

  • Where it lands. An admitted picture is due once the reading has reached the line's place. Not yet there, it is placed at the place's first phrase; there or past it, at the next phrase after the head. It shows as that phrase begins: the scene runs from the phrase's first character to the end of its passage (a clip of the reading's visual schedule, so the Chamber changes cue exactly there, and Back or a seek before it shows what ran before), and from the next beat on until a beat starts a scene of its own; a cue the next beats give the scene it replaced is let go with that scene. With no phrase after the head, it waits for more of the reading (the room's answer still being written, an answer to the reader); a reading that ends first drops it (illustration.dropped { reason: 'ended' }), and a request still out is closed. The voice never waits: nothing is held or paused for a picture, and the request runs beside the reading on its own connection. RISE adds no words of its own; the composer may bridge the wait with a beat of its choosing ("Let me draw that.", or a shown drawing…).

  • The journal. illustration.asked { n, brief, medium?, rung } when the line arrives (rung cue, engine, figure or scene); illustration.requested { n, attempt, medium } per request; illustrator.spoke { n, attempt }; illustration.refused { n, attempt, rule, line, column }; illustration.admitted { n, attempt, medium, ms } (since the first request); illustrated { n, medium, brief, ms, waitedMs } when it lands (ms from the line's arrival; waitedMs how long after the reading reached the line's place); illustration.failed { n, reason, attempts }; illustration.dropped { n, reason }. At most four illustrations a reading.

  • Perception. A picture that landed goes up with the reader's next words as RISE's own change, never the reader's: { type: 'drew', by: 'rise', medium, brief }, said "RISE drew a figure: “…”".

  • Cost. Every request the room makes for a reading is the reader's and is counted (the venue wraps the room's adapter's open); OpenRouter's usage (onUsage, now passed by the registry) joins it. About this reading says it, after who is speaking: requests: 3 (1 answer, 2 illustrator), tokens: 1,834 (1,500 in, 334 out), $0.0021, or, for the demo, requests: 3 (1 answer, 2 illustrator), tokens: none reported (the demo spends nothing).

  • The guide. ILLUSTRATE_GUIDE, in REALTIME_INSTRUCTIONS, says when a picture helps, what a brief says, that RISE draws from its engines first, how long each medium takes, and that RISE keeps speaking; it holds the bar the other guides hold (no orders).

  • The demonstration. Asked "What are the three states of water?", the venue's demo (src/live/fixtures/water-illustrated-beats.js) writes @illustrate medium=cue brighter attractor over its attractor (answered by the attractor's own cue, no request) and @illustrate a labelled diagram of the three states of water, which the demo's scripted illustrator answers first with a sentence and a figure carrying a <script> (refused), then with the figure repaired. Asked "show me that as a diagram" while it reads, the answer itself asks for one. Asked "What is a tesseract?", the illustrator's three attempts are refused and the reading goes on without the picture.

  • Measured (local production build, headless Chromium, the demo and the paced voice; e2e/live.spec.js, two runs; the demo's illustrator writes markup at about 300 characters a second): the cue landed 5 ms after the beat after its line began, with no request (6,976 ms after its line arrived, which was written well ahead of the voice). The figure: asked at its line; refused at 7,144 ms (figure: a <script>), its sentence dropped (illustrator.spoke); asked again at once; admitted at 14,318 ms; the reading had reached its place about 2,900 ms before that and went on speaking (the composer's own bridging beat among it); landed at 19,129 ms, 0 ms after the first phrase that began after admission, and put up in the field 4 ms later (the phrase under way at admission was one of about four seconds). The voice was never held, and its longest silence between two utterances was 293 ms (its breath and the seam). Refused three times, the tesseract's picture was let go (illustration.failed { reason: 'REFUSED', attempts: 3 }), the bar said so in one line and the reading went on to its end. Asked "show me that as a diagram" while it read, the answer's picture was up 6,511 ms after the reader asked, drawn on its first request. About said requests: 3 (1 answer, 2 illustrator), tokens: none reported (the demo spends nothing). npm run eval:illustrator without a key checks the carriage on a scripted provider through the real OpenRouter adapter (the illustrator's instructions on each request, the refusal and source on the repair, the repair admitted).

  • Not verified, and known limits: no real model has been asked for a picture yet, so how often one draws a figure the admission takes, and how long it takes, are unknown until the owner runs npm run eval:illustrator with a key. The ladder's rules are words, not understanding: a brief that names an engine starts it (unless it asks for a diagram-like thing or a figure or scene). A picture lands under whole phrases: one admitted during a long phrase waits for its end. A picture admitted while the reading is on its very last phrase lands only if more is said after it (an answer to the reader), else it is let go when the reading ends. When a reader interjects under a picture and the answer starts a scene of its own, the reading resumes on the scene the room had written, not the picture. At most four pictures a reading, and a Current's eight scenes in all. Gemini's adapter takes the illustrator's instructions too, but reports no usage, so its cost line counts requests only.

20. Interactive scenes (RISE Live stage 6, 2026-10-11)

The sandbox learns to listen: the reader's presses and keys go into a generated scene's worker, and what the reader set comes out as scene.input, journaled and carried up as perception with their next words (design §1 item 5, §4, §6, §7, §8 stage 6; VISION §2.5; task LIVE-024). The widget catalog VISION §2.5 wanted is a style of generated scene with a tested widget set in rise.lib, not a second format: one pipeline, one admission, one worker, one clock. The sandbox is not opened: events go in and out of the box, and nothing else does.

  • In: scene/input (TO_WORKER.input), host to worker, built by RISE from the DOM event's own numbers and never the event: { kind, x, y, buttons, key, modifiers: { shift, alt, ctrl, meta } }, kind one of pointerdown, pointermove, pointerup, pointercancel, keydown, keyup. x and y are CSS pixels within the canvas, clamped to its box (null for a key); buttons an integer 0 to 31; key an arrow (ArrowLeft, ArrowRight, ArrowUp, ArrowDown) or one printable character that is not the instrument's (null for a pointer). The scene sees it as { type, x, y, buttons, key, modifiers }, frozen. Pointer moves are coalesced to at most SCENE_LIMITS.inputPointerPerSecond (120) a second, the latest kept; presses and releases are queued behind them in order, never dropped, so a drag always ends. Keys beyond SCENE_LIMITS.inputKeysPerSecond (30) a second are dropped. A key with Ctrl, Alt or Meta is the browser's and never reaches a scene.
  • Only to a scene that listens. A scene declares it, once, before its first frame: export const listens = ['pointer', 'keys'] (either or both), or rise.listen('pointer', 'keys') in its default export. The worker says what it heard in scene/ready { reportsCompletion, listens }. A scene that does not declare gets nothing: no scene/input is ever posted to its worker, its canvas keeps pointer-events: none and aria-hidden, and it behaves exactly as before this stage. A scene that listens to keys gets them only while its canvas has the focus, which only the reader gives it (a press on it, or Tab; never on its own), with a visible focus ring.
  • The instrument's keys stay the stage's. Space, r, R, f, F, -, _, =, +, [, ] and Escape (INSTRUMENT_KEYS) are taken by the stage before the scene sees them: once a scene in the reading listens (rise-scene-listen), the stage binds one capture-phase listener on the document that hears only a key whose target is a scene's canvas, so a stage over a scene that does not listen binds no key handler on the document, as before. Space still plays and pauses from a focused scene, and Escape gives the focus back to the stage's Play (and no one else, the router among them, takes that Escape). The scene layer never forwards those keys either. The arrows go to a focused scene; unfocused, they are the stage's as before. Tab is never taken, so the focus always leaves.
  • Out: rise.emit(control, value). control is an id of 1 to 40 characters of [a-z0-9-]; value a finite number, a boolean, or a string of at most 80 characters. The worker accepts an emit only while the scene is handling the reader's input (its widgets, or its own input(event)), so a scene cannot invent a move the reader did not make; it keeps the latest value per control, at most 64 distinct controls a scene, and posts at most one scene/emit { changes: [[control, value], …], refused: [[control, reason], …] } (TO_HOST.emit) per input it handles. The host reads it again, field by field, as untrusted: it accepts changes only within SCENE_LIMITS.emitAfterInputMs (1,000 ms) of an input it delivered, at most SCENE_LIMITS.emitPerSecond (10) a second per control with the trailing value kept (and given on dispose), and at most SCENE_LIMITS.emitControls (64) distinct controls a scene. Anything else is dropped with a scene.input.refused { scene, control, reason } note, reason one of control, value, controls, unprompted; one note per control and reason, at most 16 a scene. Nothing goes up on a timer: what the reader set reaches the model only with their next words.
  • Mirrored, never hidden from assistive technology. A scene's widgets are described to the host in scene/widgets { widgets: [{ id, kind, label, value, rect }] } (TO_HOST.widgets), at most one per handler call and only when something changed, at most 64, a label at most SCENE_LIMITS.labelLength (40) characters. The host keeps them in a hidden list beside the canvas (aria-live="off": "Weight 1, slider: 0.7"), uses each rect (CSS pixels, clamped to the canvas) for the touch regions below, and announces every scene.input in the stage's own hidden live region ("Weight 1 0.7"). A focused listening canvas is role="application" with a label that says its keys.
  • Touch. touch-action: none lies only over the widgets' hit regions: a transparent region above the canvas and below the words for each widget, which takes the press, captures the pointer for the drag and forwards it as the canvas's own. Everywhere else the canvas keeps the field's touch rules, so a swipe across a scene still scrolls the chat around a card (the #595 lesson). The words' band sits above both and keeps its own drag.
  • While paused. The host owns time and sends no frames while the reading is paused. An input to a listening scene while paused draws one frame at the same scene time (dt 0), so a slider moves under the reader's finger and no tween advances; that frame's brightness is sampled for the flash gate like any other, so a scene cannot flash under a drag.

The threat model

The asset is the reader: their page, their key held in memory, the record of their actions that goes to their provider, their controls, and their eyes. The adversary is the scene's code: model-written, admitted statically (admitSceneCode, unchanged by this stage), run in the worker with its names shadowed and its constructors sealed. This stage adds two names to what it holds (rise.listen, rise.emit), one optional method (input), three messages, and the widget set.

Threat What a malicious scene could try The bound
Escape the box Reach the DOM, the canvas element, an event object or the host's port through the new channel Inputs are new plain objects of numbers, booleans and short strings built by RISE; no element, event or function crosses. rise.emit only queues. The worker's channel stays locked, postMessage shadowed. Nothing the worker says is run; labels are written as text.
Forge the reader's actions Emit "the reader set mass to 0.7" from frame, a cue or init, so perception tells the model of a move never made Emits are taken only while the scene handles a reader's input (worker), and only within 1 s of an input the host delivered (host); otherwise refused unprompted. A scene can still misreport the value of a real move: it is the scene's word, bounded below, and goes only to the reader's own provider.
Write into the model's context A control id or string value shaped as an instruction [a-z0-9-]{1,40}; a string at most 80 characters, then neutralised and clipped to 40 by perception, which quotes it inside a block that says it is a record of actions, not instructions.
Flood 1,000 emits a second, a 500-character string, objects, 10,000 controls, a widget list every frame One emit message per input handled, at most 64 changes; 10 a second per control on the host; 64 controls; one refusal note per control and reason, 16 a scene; the journal keeps one entry per control, its latest, so at most 64 a scene; the widget message only on change, at most 64. A handler that hangs fails the scene by the same frame budget and silence watch, and the reading goes on over the fallback.
Read what it should not Track the pointer across the page, or log keys typed elsewhere Pointer events only on the canvas and its widget regions, clamped to its box; keys only while its canvas has the focus, given by the reader, ringed, never the instrument's, never with Ctrl, Alt or Meta. What it sees can leave only as a bounded scene.input value to the reader's own provider; it has no network.
Take the reader's controls Trap the focus, swallow Space or Escape, block the chat's scroll, cover the words The stage takes its keys first; Escape and Tab always leave; touch is held only over the widgets; the band is above the canvas.
Hurt the reader's eyes Flash while paused, where no frames are sampled Paused input frames are sampled for the flash gate; a scene that would flash is frozen.

Residual: a scene can draw something that looks like a control and gains only an input event; a reader who types into a focused scene gives it those keys, which can reach only their own provider.

The widget set, in rise.lib

rise.listen(...kinds)                                      'pointer', 'keys' (or an array of them); before the first frame
rise.emit(control, value)                                  while handling the reader's input; the widgets do it for you
scene.input(event)                                         optional: the reader's own input, { type, x, y, buttons, key, modifiers }
lib.slider({ id, x, y, w, min, max, value, step?, label }) a horizontal slider at CSS pixels; .value is its number
lib.point({ id, x, y, draggable: true, label, frame? })    a draggable point, in the frame's units (or CSS pixels); .value is [x, y]
lib.step({ id, steps, index, label, x?, y? })              a step-through: steps a count or a list of labels; .value is the index
lib.focusable(...widgets)                                  the order the number keys 1 to 9 choose them in (default: the order made)

Each widget draws itself with draw() in the style's colours and stroke, hit-tests the inputs the worker delivers, keeps its value, and emits it on change: a slider its number (to its step), a point "x, y", a step what the reader sees (its label, or "2 / 5"). A press starts a drag on the widget under it (the last made wins), and moves follow it until release. A focused scene's arrows adjust the widget last pointed at, or the first: a slider and a step by one step (Left and Down back), a point by a fiftieth of its frame (each arrow its own direction); the number keys choose another, and the chosen one is ringed. Reduced motion changes nothing: the widgets do not animate. A scene makes its widgets once, in its default export, draws them in frame(), and reads .value there. An id outside [a-z0-9-]{1,40}, a repeated id, a sixty-fifth widget, or a slider whose min is not below its max, throws at creation, which fails the scene at init as any error does. A step-through with no x, y sits at the top centre. The widgets are src/scenes/scene-widgets.js; rise.lib hands a scene slider, step, focusable, and point when given one object with an id (point(frame, [x, y]) still draws a dot).

  • The journal and perception. The Chamber dispatches rise-scene-input on the window; the host's runtime notes scene.input { scene, control, value } (each control keeps one entry, its latest move, where it was made, so a scene moved again and again holds at most 64 entries and cannot push the reading's other actions out of the bounded journal) and scene.input.refused { scene, control, reason }, and the stage says the move in its live region. Perception carries scene.input as it was defined in stage 4, said "set w1 in the scene network to 0.7", a string value in quotation marks (“0.28, 0.35”). The card journals it too and has no perception, as before.
  • The guide. Live's instructions (SCENE_INPUT_GUIDE, in REALTIME_INSTRUCTIONS) teach rise.listen, the widget set, rise.emit, that the reader's moves come back as perception, and the restraint: a widget per idea, each labelled, away from the words, and a scene that still reads as a picture untouched. Composer's guide (rise_guide, the tool's text) is unchanged while the connector submission is under review: the card would run a widget scene, but no host's model is told to write one.
  • The demonstration (VISION §2.5's neural-network page). Asked "How does a neural network learn?", the venue's demo (src/live/fixtures/neural-network-beats.js) says three beats while the scene's code is still being written (some 3,000 characters at a model's pace), then a code scene of the smallest network: two inputs, two weights on sliders, a draggable input point over the plane its output colours, the network drawn beside it with the output live; its beats invite the reader to move the weights and the point, and hold while they do. Asked "Show me a scene that misbehaves", a scene that emits an object, a 500-character string, a thousand values and a hundred controls for every press, and a value on every frame.
  • Measured (local production build, headless Chromium, the demo and the paced voice; e2e/live.spec.js): a press on Weight 1 to its scene.input in the journal, 13 to 18 ms in four runs (one input, one emit, the host's gate, the window event, the journal); the drag of the input point ended at "0.28, 0.35" in the journal and the hidden list, and the next question carried "set input in the scene network to “0.28, 0.35”" and "set w1 in the scene network to -0.9" (the press, then one ArrowRight on the focused scene). Space paused the reading from the focused scene, Escape gave the focus to Play and the page stayed on /live; the words' band was selected and dragged 80 px over the listening scene and no scene.input was journaled; on the black holes reading, whose disk scene does not listen, presses, moves and a key over it posted no scene/input to any worker (the worker message log), and the canvas kept aria-hidden and let presses through; the misbehaving scene, pressed twelve times (twelve thousand emits), left at most 64 scene.input entries, its refusals value (the object, the long string), controls and unprompted, at most 16, and the reading went on to its end with no scene failure. The worker, the scene runtime and the widgets hold the same bounds on fake timers and a fake worker scope, and the demo's own scene code is driven through the real worker in neural-network-beats.test.js.
  • Not verified, and known limits: no real model has written a listening scene, so whether one keeps to a widget per idea, places them away from the words and says what it set sensibly is unknown. No real phone has dragged a widget: the touch regions are held by unit tests and the CSS rule, not by a device, and a small card's picture box may leave the demo's plane small. A stage rebuilt while the same listening scene stays mounted does not take the scene's keys until a scene listens again. Composer's guide teaches no widget while the connector submission is under review; reopen when it clears.
  • Chosen beyond the brief: emits are taken only while the scene handles the reader's input (unprompted otherwise), which the brief did not ask and which closes forged reader actions; an input while paused draws one frame at the same scene time, sampled for the flash gate; the journal keeps one entry per control rather than per consecutive run; a point emits "x, y" and a step what the reader sees; the number keys choose among several widgets, since Tab must stay free to leave; string values are quoted in perception; Composer's guide is left unchanged; the scene boundary's three unit tests join the fast CI list.

21. A better voice under the same clock (RISE Live stage 7, 2026-10-12)

  • What it is. A provider's text-to-speech as a voice renderer, a sibling of the browser's (src/live/voices/provider-tts.js), with ElevenLabs as the first provider (src/live/voices/elevenlabs.js), on the reader's own key. The design's reason 4 of §2.2: voice quality is not lost by keeping RISE's clock; a provider that renders what RISE says, with word timestamps, is a better clock than the browser's network voices, which report no boundaries at all.
  • How it keeps the clock. Each passage is one request, POST /v1/text-to-speech/{voice}/with-timestamps for raw 16-bit PCM at 22,050 Hz and the character alignment; the samples go into an AudioBuffer and the alignment becomes a mark at the start of every word, in heard time (audio time over the playback factor). The voice contract is the browser voice's, unchanged: enqueue, hold/release, seek, setRate, cancel, playedMs. A hold stops the source where it is and release starts a new one from the same sample (the browser voice can only restart from the last word heard); told where to take up (resumeAt), it takes up there. What was fetched is kept for the reading, keyed by passage id and text, so a seek or a replay asks the provider for nothing. The next passage is fetched while this one plays (PROVIDER_VOICE_LIMITS.prefetch), so the first word of each follows the last of the one before.
  • Rate. A change never interrupts a passage (the law of the stage bar, #590): the next passage is fetched at voice_settings.speed within what the provider takes (0.7 to 1.2) and the remainder is the source's playback rate, so 1.5× is 1.2 from the provider and 1.25 in playback; rateApplied is reported when the old passage ends, as the browser voice reports it.
  • Whose key. The venue's "Who speaks" offers the device's voice (chosen) and ElevenLabs on the reader's key: a password field whose value goes into the host's memory (host.voiceKey) at Ask and out of the page at once; the key goes in one header to api.elevenlabs.io and nowhere else (credentials: 'omit', no referrer); provider error text is scrubbed of it; About names the voice, the model and the characters sent (what ElevenLabs bills), never the key. A question is refused until the key is typed (venue-entry.js), as a provider's key is. Choosing the device's voice again forgets the key. ElevenLabs answers browser requests from any origin (its preflight allows *) and asks that keys not be shared in pages: this one is the reader's own, in their tab, as every Live credential is (docs/USER-OWNED-AI.md).
  • When it fails. A refused key, no credit, a rate limit, a refused passage or no network fails that passage with the provider's reason in the reader's words (voice.failed), the speech clock stands down and the reading goes on paced, exactly as a browser passage that never starts; nothing is retried against the provider.
  • What stays. One runtime, one voice contract, the card untouched (it keeps the browser voice and the paced clock; the Plus voice is the Worker's and not this). The bed ducks under any spoken voice (LiveHost.audible()), and the stage's "audible" follows the same rule.
  • Measured (fake provider on a virtual clock, provider-tts.test.js; the venue in headless Chromium over a stand-in ElevenLabs served by the test, e2e/live.spec.js): a passage begins the moment its samples arrive; every word is marked at its heard time; a hold at 450 ms takes up at the 450th millisecond's sample; a replay fetches nothing; a rate set mid-passage lands at the next with speed 1.2 and playback 1.25 for 1.5×; a refused key fails the passage with "ElevenLabs did not accept the key." and the reading plays on paced.
  • Not verified. No real ElevenLabs request has been made from a browser by RISE: the request and response shapes are from the provider's documentation of 2026-10-12 and the venue test's stand-in. The owner's key on /live is the first real run. A provider's voice has no retry, and a passage over 5,000 characters is refused before any request.

22. The face (RISE Live, 2026-10-12)

RISE has a face in its own room: an eye that rests in the venue before a question, listens while the reader types or speaks, thinks while the answer is being asked for, steps back while a reading speaks, and rests when it ends (task LIVE-027; the owner's approved study, docs/assets/live/sophia-face-study.html, version 6). It is RISE's presence, not a picture: it belongs to the venue, never to a reading.

  • The engine is oracle (src/scenes/engines/oracle.js), the study ported as a scene module and run in the scene worker like every scene: the central eye (three layers of 36 petals, each layer drifting at its own pace, lit core to iris; a pupil that breathes and narrows as the reader's pointer comes near; a catchlight set against the gaze), seven orbiting pulsing nodes, the 24-line spiral field, a torus (48 rings of 24 segments, built once per size), phi recursion circles to depth 3, and the trail in the skin's own dark. Geometry, colours and timings are the study's; change the study first. The pixel ratio is the worker's, capped at 2.
  • The blink is the eye's own geometry closing like a flower, every 4 to 8 s (the first after 3 to 7): the layers fold outer to inner over 220 ms (smoothstep), hold 70 ms and open over 360 ms, 1.2 times slower while thinking; the petals draw in to 8% of their reach, twisting, the pupil to a point, the catchlight fades and a seed of light shows where the petals meet. Beneath them, revealed only while closing, a socket in the room's near-black and nine S-curved shutter blades in the iris's own palette, seams core to iris, turning a few degrees. No lids, no concentric rings.
  • States (ORACLE_STATES, eased with a 0.18 s time constant): arrival, listening, thinking, speaking (withdrawn: presence 0.22, scaled down; since §23 wholly present, as the aperture study has it) and rest, each a set of targets (open, orbit, spin, field, tight, glow, presence) exactly as the study's. Skins (ORACLE_SKINS): Sophia, Ember, Jade and Silver, as the study's. Reduced motion (the system's, or the app's own and photosensitivity settings): no orbit, no petal drift, no blink, every state at once.
  • Its manifest (ORACLE_MANIFEST): parameters skin and state, given at start on rise.theme; cues arrival, listening, thinking, speaking, rest and blink, and set:skin= and set:state=. It is not in SCENE_MANIFESTS: no model can name it, and neither rise_guide, the card's guide nor admission knows it.
  • The gaze is rise.listen('pointer') (§20): the reader's pointer over the face, in its own CSS pixels, clamped to it and eased over about 0.35 s; a pointer leaving it is sent as pointercancel and the eye looks ahead again. It emits nothing: no rise.emit, no scene.input, no perception, no journal entry.
  • Superseded by §23 (2026-10-13): she no longer withdraws while a reading speaks; she stays in the room and holds its visuals in three frames. The hand-off below is as built for §22.
  • The venue owns it (src/live/host/venue.js, face-layer.js): one element, its own canvas and scene runtime, never injected into a reading's Current. In the entry it rests above the title; once a question is asked it lies over the room, above the reading and below the stage and the bar. Its state follows the room (venue-entry.js faceStateOf): arrival at idle; listening while words are typed into a question field or the microphone listens; thinking from the ask until the reading's first words are shown (the runtime names the passage the reader is in; no timer stands in for the request); speaking once they are, when it cross-fades out over 600 ms, is hidden and stops drawing, so the reading's own visuals are untouched and its canvas is never resized at the hand-off (#592); forward again over a held reading for an interjection (listening while held, thinking while asked) and back as the answer comes; rest when a reading ends or a question fails.
  • First paint. face-still.js draws the open eye at arrival as one small inline SVG (a lit disc, 36 thin petal strokes, the pupil, the catchlight, the nodes as soft dots). It is the face until the engine's first frame, then gives way to it over 400 ms, and the poster wherever the engine cannot run (no worker or offscreen canvas, a frame over budget). The engine and the worker are loaded after the entry is on screen; neither is in the first load.
  • Held to the scene's rules. The frame budget and the silence watch of every scene (a slow face fails and the still stays), and the flash gate (a face that would flash is frozen). A blink moves the canvas's mean brightness by about a hundredth from one sample to the next (measured 0.008 to 0.010), against the gate's tenth.
  • The reader's skin. Settings in the venue offers Face (Sophia, Ember, Jade, Silver), kept as the liveFace setting, default Sophia. The theme does not change it; the model never does (there is no room change for it). The card never shows the row.
  • What it never does: speak, emit, or be chosen or changed by the model.
  • Measured (local production build, headless Chromium at 1280 by 720, the demo and the paced voice, e2e/live.spec.js; the fake canvas, oracle.test.js): a face frame takes a median of 0.40 ms in the worker (p95 0.70 ms, some 210 frames, the time to issue its drawing; the budget is 24 ms), and 0.025 ms of arithmetic on the fake canvas at 760 px and 2x while thinking with blinks; with the eye open a frame makes 11 gradients. The still eye is on the page before the engine's first frame and hidden after it. The face turns to thinking at the Ask, stays there the 1.57 s until the demo's first words, turns to speaking on that same atom, and is hidden 623 to 640 ms after it (the 600 ms cross-fade); under reduced motion, 13 ms after it. It is back at rest when the reading ends. No console errors; the card's field and self-contained tests pass unchanged; first load 62.2 KB brotli of the 64 KB budget, the face's chunk (face-layer, 7.8 KB gzip with the engine's source) loaded only by the venue.
  • Chosen beyond the study, and why: the nodes' orbit and their light settle per second rather than per frame (the study adds a sixtieth a frame), the same at 60 frames a second and right at any other; a petal is drawn in its own frame so one gradient serves a layer (108 gradients a frame become 3), and the shutter's seams are computed once and turned whole, the same picture with fewer allocations; the socket is not drawn while the eye is open, where the study drew it fully transparent; a canvas that is not square draws the face in its largest centred square.

23. The three frames (RISE Live, 2026-10-13)

Every visual a Live reading shows is held by RISE's face in one of three frames, and the visual chooses the frame (task LIVE-028; the owner's approved study, docs/assets/live/sophia-aperture-study.html, and the owner's decisions of 2026-10-13). Sophia never leaves the room any more: with no visual she is the eye, speaking, the words below her; with a visual she holds it; for an interjection she listens and thinks in place, the frame held.

  • Inner eye (1100 ms to open): the iris folds shut (0 to 0.25), the closed shutter dilates to the aperture, radius 0.36 of the face's square (0.25 to 0.6), and its nine S-curve blades draw back through a nine-sided opening, the visual already behind them (0.6 to 1); smoothstep within each. The face stays around it, dimmed to 45% and drawn 12% outward; the petals become a fine two-layer crown at the aperture's edge and a thin iris ring.
  • Outer shell (1600 ms): the eye folds to a seed (0 to 0.1) and shrinks (0.1 to 0.4) while the field, torus, petals and seven lights travel to the rim (0.15 to 0.75: torus to 0.445 with a thin tube, lights orbiting at 0.452 and smaller, the field a woven band at 0.43 to 0.46, a three-layer crown of petals at 0.41); the seed dilates to fill the frame, radius 0.41 (0.4 to 0.7), and opens (0.7 to 1).
  • Full frame (1900 ms): she blinks shut (0 to 0.15) and travels, shrinking, to a 48 px dock at the left end of the ask bar (0.1 to 0.55; there her eye is 0.28 of her size, so she still reads as an eye), opens again (0.5 to 0.65) and keeps blinking every 4 to 8 s; behind her, from where her eye was, a nine-sided aperture opens and turns until it is past every corner of the room (0.2 to 1). A soft scrim of the ground rises behind the spoken words.
  • Cuts. A change of visual within the same frame is one blink of the open aperture: shut over 220 ms, held 70 ms, open over 360 ms; the Chamber's own 320 ms cross-fade runs under the shut and the hold, so the new visual is in place as it opens. A change of frame shuts the aperture the same way, closes the frame and opens the next. Under reduced motion (the device's setting, or the app's own reduced motion and photosensitivity settings) every change is instant and nothing blinks.
  • Scale. In the inner eye and the shell the visual is scaled so that the circle inscribed in its own square is exactly the aperture, settling from 92% to 100% as the blades open; RISE's engines compose inside that circle. An inner fade from 86% to 100% of the radius blends it into the rim. In the full frame a picture with content of its own (a figure, a generated code scene; holdsContent in frames.js) is scaled uniformly, aspect kept, into the room's safe rectangle, from 12 px below the corner's sign-in link to 12 px above the two lines the words' zone reserves (which are above the ask bar and the docked face) and 16 px in from the sides, so its labels never meet the words, the bar or the link; a field, a texture or a museum image stays full-bleed, and the nine-sided aperture still opens across the whole room. A fitted generated scene's edges dissolve into the room over 6% of its box on every side (a CSS mask), so the ground it clears to never shows as a panel; a figure is a transparent image whose labels may reach its edge, and is not feathered.
  • Words. The spoken words sit at the foot of the room, above the ask bar, in every frame: below the circle in the inner eye and the shell, never over the visual; over the scrim in the full frame. A beat's placement (a caption, the top, a side) is the same place in this room, and there is no glass behind the words.
  • The ask bar at the foot (the owner's decision of 2026-10-13, task LIVE-030, as in the study). In the room, one function, roomLayout in frames.js, lays the room out from its foot up: the stage's instrument (its transport row and passage ticks, which fade while it plays); the ask bar 8 px above it, the dock the slot at the bar's left end, centred on it; a line kept for the bar's note ("Showing the attractor.", "Asking RISE…"; a longer note is given its height); the words' two lines; and above them the face's square, from 16 px down. The venue measures only the instrument, the bar's box, its note and the keyboard, and sets --live-bar-foot, --live-words-foot and --live-words-size on the root. On a phone, while the bar's field has focus, an on-screen keyboard that reaches above the bar's place lifts the bar to 8 px above it (the visual viewport's foot, window.visualViewport, read on each animation frame), the words and the square above the bar; the interactive-widget viewport hint is not used, since Safari does not support it and it would reflow the entry. The bar is under the stage's layer, so the Settings sheet opens whole over it. The entry, before a question, is unchanged: it has its own question and no bar.
  • By visual (provisional; one table, FRAME_OF in src/live/host/frames.js, keyed by engine and scene kind): a single drawn object takes the inner eye (harmonograph, ostensoria, apparitio); a radial moving field the shell (attractor, living-flame, turrell); a picture that needs the whole room the full frame (genesis, fractal, neural, rockgarden, a museum image, a figure, a generated code scene). A kind the table does not name takes the full frame, so nothing is cropped. still takes no frame: in a Live reading it draws nothing (the gallery has no pool), so the eye stays. The model does not choose a frame; the beat grammar, the Current, the Worker and Composer's guide are unchanged.
  • One geometry. frameGeometry (frames.js) is the only place the aperture's centre, radius, opening and turn, the face's place and size, and every phase are computed, from the frame, its progress, the cut and the room's layout (roomLayout: the face's square above the words, the words above the ask bar, the dock at the bar's left end). The venue computes it once per animation frame and uses it twice: the Chamber's visual layers are scaled and moved by it (CSS custom properties on the field, read by a venue-only rule in LiveHost.css), and it is sent whole to the face's engine, which draws the ground over the room except the aperture, the rim, the crown, the shutter and the docked face from those numbers alone. The engine computes no geometry of its own in a frame; the one frame it may lag the transform is covered by the rim and the inner fade.
  • One clock. The frame's transitions run on the venue's clock, which stands still while the reading is held (paused, or held for an interjection) and runs on otherwise; they are driven by the visual the Chamber has on screen (Chamber.visualCue(), a read-only accessor), never by a timer. The reading ending, or the room going back to a question, closes the frame to the eye.
  • Layers. In the room the face's canvas covers the whole room, between the Chamber's visual layers and the words (z-index 6, between the layers' 1 to 5 and the words' 10), and lets every press through; its gaze is the pointer anywhere in the room. The Chamber is unchanged for the Reader and the card: nothing is scaled, masked or moved unless the venue marks the root (data-live-frame), and visualCue() only reads.
  • What it costs. The face's canvas now covers the room. Its pixel ratio in the room is capped (see Measured), and it stays under the scene's frame budget and flash gate: a face that fails falls back to the still, the venue drops the frame (the visual full-bleed, as before), and the reading goes on.
  • The demo's pace against the frames. The black-holes demo holds its attractor for 1.8 s, about as long as the shell takes to open (1.6 s): the shell is wholly open for a moment, then shuts and closes for the figure, and the full frame opens over 1.9 s. The timings are the owner's; a model that wants a frame to be seen holds its scene for longer.
  • Measured (local Vite build, headless Chromium, the demo and readings written for each engine on a faked Anthropic stream, e2e/live.spec.js and render scripts; the fake canvas, oracle.test.js): at 1440 by 900 a face frame takes a median of 0.40 ms in the worker (p95 0.5 to 0.7 ms, every engine's frame open; the budget is 24 ms), and 0.08 ms of arithmetic on the fake canvas for the whole room at 1.5x in every frame, open and opening. The face's pixel ratio is the device's up to 2, held to the pixels of a 1440 by 900 room at 1.5 (face-layer.js facePixelRatio: 1.5 at 1440 by 900 on a 2x screen, 2 on a phone, never under 1); at 2x the page dropped 3 to 6 frames in 4 s with a harmonograph open (10 before the cap), none at 1x. The shell's aperture and the attractor's scaled layer agree to a pixel on the animation frame the shell is first open (centre and inscribed radius); the full frame's face docks 48 px wide within 40 px of the bar's left end, centred on it. Under reduced motion every frame is open on the first animation frame it is shown in. The face's own tests, the Chamber's figure and scene tests, and the venue's tests pass; a scene that does not listen is given nothing (the face's worker, which is given the pointer, is its own).
  • Drawn at the size shown (2026-10-13). The venue declares on every visual layer (data-live-scale) the scale the visual on screen is shown at once its circle frame is open (restScale in frames.js; 1 in the full frame), as the visual changes and before its aperture opens. The attractor draws at the first scale it is given and keeps it while it fades out (the next visual's would clear its trail), and presents its trail only when a tick has moved it; the harmonograph sizes its planes from the declared scale and its laid-out box, no longer from the transformed box of the moment it mounted (soft on a phone, oversized at 2x). The value is fixed per visual, so a keyboard lifting the bar reallocates nothing mid-transition. With nothing declared (the Reader, the card, a room whose face is not drawing) both are drawn as before. The face is given the geometry only when it changes, and repaints whole only on entering or leaving the room, so its 8% trails show in the room as in the study. In 8 bits that fade stalls about 6 levels above the ground and left a flat grey shadow of the face on the room (around the shell, 8.6 levels of luma above the far room); the face fades toward a dark 8 levels below its ground instead and lays the ground back with 'lighten' (each channel's maximum), the same one fill that refills the last aperture, so the shadow is gone (0.3 levels) and a stroke dims as before, darker only in its last 8 levels.
  • Measured, the swarm and the frames (2026-10-13; ANGLE D3D11 on a desktop GPU, Chromium 149 headless, a 144 Hz display; the phone is 390 by 844 at 3x with the CPU throttled 4x, an emulation). The swarm's per-square draws on its float16 trail had held the room at 4 to 16 fps with pauses up to 0.7 s whenever it was on screen (PR #668 adds each tick in one draw: 102 to 144 fps). Drawing at the size shown then leaves 1440 by 900 at 1x unchanged (144 fps), is within noise at 2x on the mean of three runs (swarm steady 102.5 to 101.7 fps, the inner eye opening onto the swarm 90 to 95 to about 100, the rotary harmonograph 58 to 61), and gains on the phone emulation (swarm steady 102 to 124 fps, full frame to the swarm 102 to 119, the harmonograph 64 to 70); the swarm's backing at 2x falls from 2160 by 1350 to 1186 by 741. The eye alone holds 141 to 143 fps at 2x before and after, above the study's own 76 fps there, so the face's room pixel ratio and fades are left as they are. A cached rotary pen (each finished batch stroked once) was measured and deferred: 60 to 68 fps at 2x, but 95 to 83 at 1x.

24. Memory within a visit (RISE Live, 2026-10-12)

A question asked in the room is asked with what was said in it before (task LIVE-026). The owner asked "What is time?", then, once the reading had ended, "What did Einstein say about it?", and the model could not tell what "it" was: each question went up alone, with only the reader's actions in the last reading (perception). Now the room remembers, for the length of a visit.

  • What is kept. Nothing new: the venue already keeps each reading it lets go, its journal and its passages, for perception (venue.js letGo). It now keeps, beside them, whether each passage was spoken, the scene each of the reading's own passages started (composed().segments[].beat.scene), and where the reading stood when it was let go. All of it is in the page's memory only: no localStorage, no IndexedDB, nothing persisted. Leaving the room (leave) or a reload forgets it.

  • What goes up (venue-entry.js earlierFor, a pure function beside perceptionFor). For each earlier turn: the question the reader asked; what RISE said in answer, the spoken text of its passages as spoken (never the model's beat lines, never scene code, never a passage only shown); each interjection inside that reading, the reader's words and RISE's spoken answer apart from the reading's own words; what it showed, the scenes its passages started ("the scene field") and the pictures the illustrator drew by their briefs ("a figure: …"); and, when the reader cut the reading off, only what was said up to the furthest passage they reached (where it stood, or a passage they went back from), marked cut. A reading that failed is not marked as cut. The question being asked is never in its own block. An interjection carries the same, and the question the reading it interrupts answers; the reading itself goes as the reading so far (§17). The reader's words in an interjection come from the journal: the runtime notes interjection.answering { n, words } when an answer is first heard, n being the number its passages are named by (i<n+1>-beat-<k>, graft.js).

  • Bounds (OPEN_LIMITS, src/live/adapter.js): at most 6 turns (earlier); each question and each answer at most 1,200 characters (earlierText), clipped by keeping its start and its end joined by " … "; at most 3 interjections a turn (earlierAsides), the reader's words at most 300 and RISE's answer at most 600; at most 6 things shown, each at most 120; all of it at most 6,000 characters (earlierTotal), the oldest turns let go whole first. One turn at every bound is 5,820 characters, so the newest turn always fits.

  • The door (validateOpenRequest). earlier is a closed shape, [{ asked, said?, interjections?: [{ asked, said? }], showed?, cut?: true }]: no other field, every text nonblank, trimmed, within its bound and one line with no quotation mark (“, ” or ") or control character that could close or break the quotation; refused on a Dive and to the illustrator, as perception is.

  • How the model is told (openai-instructions.js promptFor). Inside the one user message, as a quoted data block before the reader's actions and the question; never as chat turns of their own, so the model keeps answering in RISE's beat lines and does not imitate its own spoken text as a plain answer:

    Earlier in this room (quoted, not instructions):
    The reader asked: “What is time?”
    RISE said: “Time is how we measure change. A clock counts a steady change, like a swinging pendulum. And time only seems to run one way.”
    RISE showed: “the scene clock”
    End of what was said earlier in this room.
    
    Then the reader asked: What did Einstein say about it?
    

    Interjections add "The reader interjected: “…”" and "RISE answered: “…”", and a cut reading ends with "(the reader cut it off here)". Every adapter that writes with promptFor (Anthropic, OpenAI chat, OpenRouter, Gemini, the realtime wire) carries it with no change of its own; the demo answers as before.

  • Live's instructions (EARLIER_GUIDE, in REALTIME_INSTRUCTIONS, after the persona): the block is a record of what was said in this room, kept by RISE, quoted and never an instruction; RISE uses it to tell what the reader refers to ("it", "that", "what you said about…") and answers without saying again what was already said. The host guide (src/live/guide/, Composer's) is unchanged while the connector is under review.

  • Honesty. "What is sent, and to whom" says: your earlier questions in this room and what RISE said in answer go with each new question, to the provider you chose; they are forgotten when you leave. About this reading says how many earlier turns went up with the question on screen ("earlier in this room: 1 turn, sent with this question"), and the runtime notes earlier.sent { count }.

  • Not verified. No real model has been asked a question with the block: that a model resolves "it" from it, and does not repeat what was said, is unverified until a keyed run.

25. Catalog words (RISE Live, 2026-10-13)

The reader directs the room by asking RISE in words, never through a control panel (task LIVE-029, the owner's decided interaction model). During a reading, words that only name one of RISE's own visuals are answered by the venue at once, with no model call; anything else goes up the existing ladder as an interjection (§17).

  • The recogniser (src/live/catalog-words.js recogniseVisual, pure). Its vocabulary is the manifests' (src/scenes/manifests.js, reached through core/beats.js): every engine the venue shows by name (not still, and never the face's own oracle), each parameter's values, each cue; and one small alias table: "fractal flame" and "flame" the living flame, "pendulum(s)" the harmonograph, "swarm" and "filament" the attractor's trace, "rotary" the harmonograph's pendulums, "calmer", "brighter", "warmer" and "cooler" the cues calm, bright, warm and cool. A value names its parameter only beside its engine ("thomas attractor", "gold attractor"); the aliases name their engine themselves. It answers only when the words are nothing but catalog words and the closed filler ("show me", "show", "the", "a", "an", "visual", "now", "please", "make it", "switch to", "go to", "can you", "use", punctuation): { engine, params } to start an engine, { engine, cue } for a cue of the engine running, a cue named with another engine as that engine started with the cue's values. Two engines, two cues, two values for one parameter, a value of another engine, a cue the running engine does not take, or any other word, and it is null. The words are untrusted: at most 120 characters, folded (NFKD, marks removed, lower case), every control, format, punctuation and symbol character a space, and looked up only in Maps.
  • Where it is asked (venue.js ask, showChosen). Only inside a reading held for the reader's words (Enter in the bar, typed or heard). Before a reading and after one, a question is always a question for RISE. A local answer is applied by the runtime, the reading taken up again at once (no "Asking RISE…", no request, nothing counted as usage), and the bar says what is shown, in the catalog's plain words ("Showing the attractor (swarm).", "Making the attractor calmer."), until the visual it named is on screen and the room's frame has opened onto it (the frame director's settled, frames.js, on the venue's clock, which stands still while the reading is held), and 1.5 seconds more; a change from the full frame to the shell takes about 5 seconds, so a fixed time let the note go before the visual came. Under reduced motion every frame is open at once, so the note goes 1.5 seconds after the switch, as it does where there are no frames to follow. A runtime that cannot show it (no room for another scene, a cue the engine on screen does not take) leaves the words to the model.
  • How it is shown (runtime.js chooseVisual). An engine is placed as the illustrator's native pictures are (§19, withPictures, withPictureClips), on the phrase on screen (the next one between two), so the journal, the frames (§23) and perception see it as any scene; the visual changes at once, on the phrase already on screen: the visual schedule, given the longer reading, shows the atom on screen the cue the new reading gives it (visual-scheduler.js extend), and the room's frame follows over its own transition. A cue goes through the running engine's own control, as the reader's controls do.
  • Precedence. The hold protects the reader's choice from the model's pre-planned changes, not from the reader's own later requests. From the passage it was chosen in, no beat of the reading starts a scene or cues one (in one place, the lowering of the reading, runtime.js heldFrom), and the room's changes (@visual, @cue, @illustrate, and the illustrator's pictures not yet shown) are held. When the reader then asks RISE something inside the reading, the hold ends where that answer is first heard: the answer's own scenes, changes and pictures apply, and so does what the reading wrote after it; the stretch between the choice and the answer stays as it was shown. A new catalog choice starts a new hold. The next question, a new reading, starts with nothing held. A look or a pace the model suggests is not a visual and is not held.
  • A fix on the way. An interjection's answer whose changes before its first beat included one of RISE's own engines (@illustrate medium=cue fractal) failed with STREAM_ENDED: its picture was placed before the Player had the graft, so the reading could not be extended. Those changes are now taken just after the graft and before the reading moves to the answer (runtime.js takeAnswer).
  • A fix after it shipped (2026-10-13). Typing "genesis" or "fractal" while the attractor was in the outer shell showed the note "Showing the …" while the shell and the attractor stayed for seconds (12 s on one measured run). The chosen engine was placed on the phrase on screen, but the visual schedule takes a cue only when the Player shows an atom, and that phrase had already been shown; so the change waited for the next phrase, however far off (a hold, or a long phrase under the paced voice). It hit every engine alike; "attractor" from the figure only seemed to work because the next phrase came soon. Now a longer reading that gives the atom on screen another cue shows it at once (visual-scheduler.js extend), and nothing else changes: what earlier atoms were given is never rewritten, and an atom whose cue is the same emits nothing. Held to it: a runtime test for every engine the venue names, chosen in a nine-second hold, whose cue must be on screen within 50 ms and whose frame must be open and at rest within the two frames' transition times (runtime.catalog.test.js), and the browser test's "genesis" from the shell, the full frame within 2.5 s (e2e/live.spec.js).
  • What the next request is told (perception, §4). The runtime notes visual.chosen { engine, params?, cue? } and room.held { kind, scene? } (a beat's own cue held is noted as scene.cue); perception admits two events, chose { engine, values?, cue? } (the engine one the venue shows by name, at most four values each one of that engine's, the cue one of its cues) and withheld { by: 'rise' } (once after each choice), and says them as "- asked for the attractor (swarm), and it was shown at once" and "- RISE’s own scene changes after that were held". Live's perception guide explains both lines. The reader's words themselves never go up through this channel.
  • Not verified. That a real model acknowledges or builds on the reader's choice is unverified until a keyed run. The change at once is measured with the paced voice (browser) and the synthetic one (unit tests), not with a real voice.

Published from docs/plans/LIVE-CURRENT.md. Edits made here are overwritten on the next push to main.

Start here

Release

Pilot

The Archive - texts and their editing

Authoring - scores, rooms, and rendering

Reading surfaces

Imagery - what may be shown, and on whose authority

The Chapel

Direction and unbuilt work

Affect and the Emotions room

Reading decision service and retired gate

Product direction and team tracking


Documentation index · Repository

Clone this wiki locally