eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1# πŸ€– Conversation scripts β€” a schema for Eleven's deterministic flows
2
3Status: **phase 1 complete β€” schema v1 is frozen against reality.** Both
4shipped conversations are pure data (`setup.flow.js`, `welcome.flow.js`)
5interpreted by the engine (`web/static/flow.js`); `test/flow.test.mjs`
6drives full transcripts of both in Node with a fake io and enforces JSON
7purity, so the flow files are shippable fixtures for the Swift interpreter.
8Next: phase 2, serving the scripts from the binary. Design captured
92026-07-07. Builds on
10`conversational-onboarding.md` (the shipped setup + welcome conversations) and
11exists because of one new fact: **the iOS and Mac apps are moving to fully
12native rendering**, so they must run the onboarding conversations natively,
13matching the web. Hand-porting `setup.js`/`welcome.js` to Swift means every
14flow tweak lands twice and drifts; keeping onboarding in a webview fights the
15native direction at the exact moment (first run) an app most needs to feel
16native. The answer is the classic split:
17
18> **Flows become data** (a script the server ships), **clients become
19> interpreters** (one engine per platform, written once).
20
21Scripts change often and must stay in lockstep across platforms; interpreters
22change rarely. Shipping the scripts in the server binary β€” like the changelog β€”
23means a flow change is a server deploy: no App Store release, no TestFlight
24lag, and web/iOS/Mac render the *same* flow version by construction.
25
26## What stays true
27
28- **Deterministic, client-executed, no AI.** Exactly as today
29 (`conversational-onboarding.md` non-negotiables): a fresh instance has no
30 sidecar and no key, so onboarding cannot depend on either. The interpreter
31 runs client-side, where `setup.js`/`welcome.js` run now; the server ships the
32 flow *definition* and never sees the answers.
33- **E2E rules unchanged.** The script is content + control flow β€” it collects
34 nothing itself. Every side effect goes through the same host-bridge verbs the
35 flows use today, each landing on an existing trust boundary (names encrypted
36 under the instance key; group name/server photo/contact email are plaintext
37 branding by design; the passkey ceremony is the existing enrolment).
38- **One voice.** Eleven's tone lives in the script strings. A copy edit is a
39 script edit β€” one place, all platforms.
40
41## What the schema must express
42
43Everything the two shipped flows actually do β€” this list is derived from
44`setup.js` + `welcome.js` line by line, and it is the acceptance bar:
45
461. **Steps with skip guards.** Each step re-checks on entry: skip if its
47 recorded `steps[id].done`, or if external state already satisfies it
48 (`myName()` set, `enrolled()` true). This is what makes reload/passkey
49 round-trip resume work.
502. **Bubbles.** One or more per step (the context-then-question pattern), with:
51 template interpolation from a context bag (`{name}`, `{space}`,
52 `{inviter}`…); an optional **ack prefix** folded from the previous step
53 under a **stable key** (so a resumed run recognises the question despite the
54 variable prefix); persisted-vs-local lines (image previews are client-only);
55 image bubbles (`img`, `imgAlt`, round); composer hints (`hint`,
56 `attachHint`).
573. **Inputs**, one of:
58 - **chips** β€” advancing (consume the step, spent past-tense label:
59 Skip β†’ Skipped), or non-advancing (passkey launch), with conditional
60 presence (`Keep β€œ{current}”` only when a name was explicitly set;
61 `I'm {suggested}` only when the invite carried a suggestion);
62 - **typed answer** β€” next composer message in the thread;
63 - **attachment answer** β€” next message bearing an image; its text rides
64 along (alt text); a non-image reply re-prompts in place and re-arms;
65 - **probe** β€” a timer + predicate (the 1.5s "did the passkey ceremony
66 navigate away?" check that routes to the stayed-in-place waiting step).
674. **Effects and branching.** Side effects are *named verbs with declared
68 result enums*, and the step branches on the result: `setGroupName β†’
69 ok | invalid | error` (each with its own reply and re-ask), `adoptProfilePhoto
70 β†’ ok | nokey | error`. Retry loops re-ask in place, not from the top.
715. **Races.** A skip chip must be able to disarm a pending answer waiter
72 (`cancelAnswer`), and an effect can be guarded so it never runs if the step
73 already resolved (the photo-vs-Skip race).
746. **Recording.** A completed step emits its `*.step` event (label + value) β€”
75 the βœ“ action lines that make the transcript real history. A step may also
76 carry a top-level `record` that persists ON ARRIVAL (once β€” skipped when the
77 step is already recorded): welcome's `finish` records its completion marker
78 this way, so a host with no run-local state (the native embed has no
79 localStorage join marker) can tell a finished conversation from a half-done
80 one purely from recorded state.
81
82**What the schema deliberately does NOT express: feel.** Typing beats, pacing
83jitter, the reply queue, persistence retry/echo-wait, spent-chip rendering,
84placeholder plumbing β€” all of that is the **engine's** job, specified once in
85the interpreter contract below. `eleven.js` already *is* this split on the web
86(scripts call `say`/`ask`/`advance`; the engine owns the rest), which is the
87strongest evidence the boundary is right. Keeping feel out of the schema is
88what keeps the schema small enough to trust.
89
90## The schema (v1 sketch)
91
92A **flow** is JSON: `{ schema: 1, flow: "setup" | "welcome", entry, steps }`.
93Steps are named; control flow is explicit `next` / branch targets β€” no
94implicit fallthrough. Illustrative fragments (the group-name step, with
95validation branching, and the passkey step, with a probe):
96
97```json
98{
99 "id": "group",
100 "skipIf": ["step.done"],
101 "bubbles": [
102 { "text": "{ack?}Next, this space needs a name. It's the one everyone sees β€” on the sign-in page, in invites, on the app icon.", "key": "q-group" },
103 { "text": { "ifSet": "space.nameSet", "then": "It's currently β€œ{space.name}” β€” keep that, or reply with a new name.", "else": "What should we call it? Reply with a name." },
104 "hint": "Type a name for this space" }
105 ],
106 "chips": [
107 { "if": "space.nameSet", "label": "Keep β€œ{space.name}”", "spent": "Kept", "goto": "email" }
108 ],
109 "input": { "kind": "text" },
110 "effect": { "verb": "setGroupName", "arg": "$answer" },
111 "on": {
112 "ok": { "record": { "label": "named this space β€œ{$answer}”", "value": "$answer" }, "goto": "email" },
113 "invalid": { "say": "Hmm, that name didn't stick β€” it needs to be 1–60 characters. Try another?", "retry": true },
114 "error": { "say": "Something went wrong saving that β€” it's not the name's fault. Give it another go in a moment.", "retry": true }
115 }
116}
117```
118
119```json
120{
121 "id": "keys",
122 "skipIf": ["bridge.enrolled"],
123 "chips": [
124 { "label": "Save a passkey (recommended)", "advancing": false,
125 "effect": { "verb": "savePasskey" },
126 "probe": { "afterMs": 1500, "unless": "bridge.enrolled", "goto": "keys-waiting" } },
127 { "label": "Not now", "spent": "Skipped", "goto": "keys-confirm" }
128 ]
129}
130```
131
132Everything in these fragments maps 1:1 to a line of the shipped JS. The full
133v1 grammar is settled during the web-refactor phase (below), where each
134construct earns its place by being *needed* to reproduce the two flows β€”
135nothing speculative goes in.
136
137## The interpreter contract
138
139Two engines, one spec. Each platform implements:
140
141- **The bridge verbs** β€” the closed set of side effects a script may name:
142 `setMyName, setGroupName, setContactEmail, adoptServerPhoto,
143 adoptProfilePhoto, setUserInvites, savePasskey, markSetupComplete,
144 markWelcomeDone, deferBackupReminders, openThread, startTour` β€” plus the
145 read-only **context bag**: `space.*`, `suggested`, `inviterName`,
146 `adminName`, `bridge.enrolled`, `bridge.myName`. Each verb's result enum is
147 part of the contract. A script can *only* name these verbs; an unknown verb
148 fails closed (see fallback). This is the capability boundary β€” a script is
149 data and can never do anything the contract doesn't offer.
150- **The feel** β€” typing beat + pacing (per `eleven.js`), the reply queue,
151 persisted `*.say` events keyed by stable key (dedupe on resume), spent-chip
152 rendering, composer hint plumbing, answer waiters with cancel.
153- **Recording + resume** β€” `*.step` / `*.say` events into the app-thread state
154 exactly as today, so a flow started on web and resumed native (or vice
155 versa) folds the same transcript. The events are the state; the script is
156 stateless.
157
158The web engine is not new code: it's `eleven.js` plus a thin step-walker
159replacing the hand-written step functions. The Swift engine is new, but it's
160the same small state machine β€” and the native feed must already render
161bot-voiced bubbles with action chips (they exist in the wire format), so the
162rendering half is shared with native chat work, not extra.
163
164## Delivery & versioning
165
166- Scripts are embedded in the server binary (like `changelog/`) and served at a
167 token-gated `GET /api/scripts/{flow}` (the joiner has a token by the time the
168 welcome runs; the setup claimer likewise). `{ schema, rev, flow, steps }`.
169- **Evolution is additive within a schema major.** Interpreters ignore unknown
170 *optional* fields; an unknown **verb, input kind, or schema major fails
171 closed**: the client abandons the script and uses its built-in fallback β€”
172 the native name form / minimal join path, which every client keeps. Failing
173 closed matters because this is onboarding: a too-old app must still let
174 someone join, just less delightfully. (Server and web ship together, so web
175 never lags; the rule exists for installed native apps.)
176- A script change is a changelog-worthy server deploy; `rev` makes "which flow
177 version did this member see" answerable.
178
179## Sequencing
180
1811. **Freeze the schema against reality**: refactor `setup.js` + `welcome.js`
182 into flow JSON + the web step-walker, behaviour-identical (the two-party
183 canary harness verifies transcripts). The schema is *done* when both shipped
184 flows are pure data and nothing else changed. This is the cheap place to
185 discover the schema is wrong.
1862. **Serve the scripts** from the binary; web switches to fetching them.
1873. **Swift interpreter** when the native Mac/iOS surfaces land β€” against a spec
188 two real flows already exercise, with the web engine as the reference
189 implementation and the JSON as shared fixtures (the same
190 vectors-from-the-real-thing pattern as `internal/e2ee/testdata`).
1914. Later, on their own merits: `/tutorial` migrates to a flow; new flows
192 (re-onboarding after restore, feature tours) become script edits.
193
194## Boundaries β€” what this is not
195
196- **Not the bot-recipe schema.** `docs/bots.md`'s trigger→action recipes are
197 the *sidecar's* declarative layer (keyword/schedule β†’ reply/nudge), executed
198 by the bot engine off-server. Conversation scripts are *client*-executed and
199 serve native parity. Siblings β€” they share the chip/bubble message shapes β€”
200 but unifying them into one behaviour language is the too-clever failure mode
201 CLAUDE.md warns about. Two small schemas beat one grand one.
202- **Not a general mini-app format.** Mini-apps stay code (`apps/README.md`).
203 A flow script cannot render arbitrary UI, only bubbles/chips/inputs β€” that
204 ceiling is the point.
205- **Not a plain-English builder** β€” though flows-as-data is the substrate that
206 would eventually make one possible (`docs/bots.md Β§ Creating bots in plain
207 English`).
208