eleven / ocho Public

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

Plain git — no account needed to clone.

Download

Download this file

1# πŸ€– Shared remote Claude Code sessions β€” an Eleven group fronting a workstation
2
3**Date:** 2026-08-05 Β· **Status:** accepted β€” open questions decided by Paul
42026-08-28 (see Β§ Decisions) Β· **Scope:** design only β€” no implementation yet
5
6## What this is
7
8A group in Eleven becomes a shared front-end to **Claude Code sessions running
9on a remote workstation** (the motivating box: docs-t1). Multiple people share
10the sessions: the chat on the left is where you steer β€” **@claude is an
11ordinary member of the chat** β€” and the right-hand pane is a live view of the
12remote sessions. Agent activity is translated into Eleven primitives (typed
13events, cards, views, action lines, notices) rather than mirrored as a
14terminal.
15
16The design uses almost entirely existing machinery β€” no new group type, no new
17E2EE exceptions, one new sidecar-shaped process and one new mini-app.
18
19```
20β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
21β”‚ Chat β”‚ agents app (Interface pane) β”‚
22β”‚ β”‚ β”‚
23β”‚ Paul: @claude fix the β”‚ β–Έ fix-login πŸ“ messenger β”‚
24β”‚ flaky reminder test β”‚ running Β· turn 3 β”‚
25β”‚ @claude [fix-login]: β”‚ βš™ go test ./internal/bot β”‚
26β”‚ Found it β€” the ticker… β”‚ β–Έ ops-poke πŸ–₯ workstation β”‚
27β”‚ Ana: @claude also check… β”‚ waiting for approval β”‚
28β”‚ β”‚ [Allow] [Deny] β”‚
29β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
30 the shipped Chat | Split | widget-bar layout, nothing new
31```
32
33## Trust model (read this first)
34
35- **The E2EE rule holds untouched.** The server relays opaque ciphertext;
36 every `session.*` event, prompt, approval, and view is encrypted under the
37 thread key like any message. The server never learns what the sessions do,
38 which repos exist, or that a box called docs-t1 is involved.
39- **The bridge is a bot-model process** (docs/bots.md): an E2EE member whose
40 "device" is the workstation. It runs ON the box, beside the sessions, holds
41 its member private key + the Anthropic access there, and is a **pure
42 outbound client** β€” dials the instance and the Anthropic API, no inbound
43 ports. Bot code never runs inside the lchat server process.
44- **Group membership is the consent moment.** Everyone in the group can steer
45 sessions, which means everyone in the group has a lever on the box β€”
46 filesystem, git credentials, whatever the bridge's unix user reaches. This
47 is the operator-only dev-bot tier (docs/bots.md tier 4): chat input is
48 untrusted input to a machine with authority. The bridge's config (below)
49 enumerates exactly what that authority is; adding a member is granting it.
50- **Flat authority for v1.** Any member can prompt, approve, and interrupt.
51 Roles (driver/observer) are layerable later as ordinary fold state
52 (`agents.role` events) without schema changes.
53
54## The pieces
55
56### 1. The bridge β€” `lchat agents run`
57
58A sibling subcommand of `lchat bot run`, reusing the Go sidecar runtime
59(WebSocket client, key unwrap, reconnect, trigger plumbing β€” all existing). It
60runs on the workstation and:
61
62- joins the instance as the **@claude member** (one keypair, one roster row β€”
63 the standard bot-member shape, not a charter persona);
64- owns the Claude Code sessions: spawns them **headless** (`claude -p
65 --output-format stream-json`, resumable via `--resume <session-id>`; the
66 Agent SDK is the same thing as a library), consumes the **structured event
67 stream** β€” assistant text, tool_use blocks with full inputs, tool results,
68 turn boundaries. **No terminal emulator**: nothing scrapes a TTY. The only
69 terminal-ish work is stripping/basic-rendering ANSI colours out of Bash tool
70 output (~50 lines, no cursor addressing exists in a `-p` session);
71- translates the stream into encrypted `session.*` events (vocabulary below)
72 posted into the group thread, and reads members' prompt/approval/interrupt
73 events back off the same thread;
74- serializes turns per session (a session is one-turn-at-a-time), queues
75 prompts that arrive mid-turn, and **attributes prompts by speaker** β€”
76 "Paul: fix the flaky test" β€” so the model knows it's talking to a room.
77
78Config, per group the bridge serves:
79
80```
81groups:
82 <thread-id>:
83 roots: ["~/github.com/elevenmessenger/messenger"] # scoped sessions live here
84 plain: false # whole-workstation sessions allowed?
85```
86
87The config is the blast radius, written down in one place on the box.
88
89### 2. Sessions β€” two flavors, chosen at spawn
90
91- **Scoped** (default): bound to a directory β€” an allowed root, normally a
92 **fresh git worktree under it** so parallel sessions don't trample each
93 other. A scoped session gets that project's own `.claude/` settings,
94 CLAUDE.md, hooks, and permission config exactly as a human dev would β€” the
95 repo's existing guardrails govern the remote session with zero new
96 machinery.
97- **Plain** (opt-in via `plain: true`): no pinning β€” cwd = home, the whole
98 workstation at the bridge's unix user's reach. The "shared computer" mode
99 for ops-ish, cross-repo work. Off by default because it hands every member
100 the entire box.
101
102Flavor is **visible everywhere**: carried on `session.spawn`, badged on the
103session's view ("πŸ“ messenger @ fix-login" vs "πŸ–₯ full workstation") and on its
104chat card. Nobody wonders which kind of hands are on the box.
105
106Birth is conversational: "@claude fix X" with zero live sessions spawns a
107scoped session in the group's default root; `/claude new <name>` names one;
108`/claude new --plain <name>` when the config allows. Session state persists on
109disk on the box (Claude Code's own session files), so a human on the
110workstation can `claude --resume <id>` from a real terminal after the bridge
111lets go β€” continuity is preserved; concurrent dual-driving is not pretended.
112
113### 3. Chat β€” @claude plus focus-based routing
114
115One bot member; the mental model is "a colleague in the chat who happens to be
116several sessions deep."
117
118- **Routing: the target is the active view.** Each member's client knows which
119 session view they have focused in the right pane; a message to @claude
120 carries `target: <session-id>` **inside the encrypted payload**. Focus is
121 per-member β€” two people can steer two sessions from the same chat
122 simultaneously.
123- **Routing is visible at send time.** When more than one session is live, the
124 composer shows a chip β€” "β†’ fix-login" β€” tappable to retarget without
125 touching the pane.
126- **Replies pin the target.** Every @claude bubble carries a subtle session
127 tag; replying to a bubble routes to that session regardless of pane focus β€”
128 this is also the mobile/chat-only answer. The tag works the other way too:
129 tapping it focuses that session's view.
130- **Degenerate cases collapse.** One live session: everything routes there, no
131 chip. Zero sessions: @claude answers as the workstation ("no sessions
132 running β€” want one?") and a prompt spawns one.
133
134Output posts as @claude with the session tag on each bubble β€” the feed reads
135as one assistant working several desks, which is the truth.
136
137### 4. The right pane β€” one app, one view per session
138
139- **One `agents` mini-app** (`web/static/apps/agents.js`), chat-placed
140 (`places: ["chat"]`), added to the group like any app β€” one glyph in the
141 widget bar, rendering in the standard Interface pane. The shipped
142 Chat | Split | widget-bar toggle IS the "chat left, session right" layout;
143 nothing new is built for it.
144- Its Interface is the **fleet board**: one card per session (name, flavor
145 badge, status, current activity line, waiting-on-approval chips), folded
146 from durable `session.*` events.
147- **Per-session status rides the views engine** (`views.js`,
148 `view.create`/`view.update` from the bridge): one engine view per session.
149 Views are declarative SCHEMA + DATA interpreted by **both web and the Swift
150 twin engine**, so session boards render on iOS/Mac with no per-feature
151 Swift. Chips on @claude's replies open the session's view; `/views` lists
152 them.
153- **Fidelity is "board + drill-in" (option c).** Tap a session to focus it:
154 the durable board plus a **live tail** β€” tool-call chips ("Ran `go test
155 ./...`"), collapsed monospace output, file-edit lines, assistant prose β€”
156 streamed as **ephemeral events** (relay-only frames, the existing
157 primitive): live viewers see the chatter; nobody replays it; the encrypted
158 log stores only what deserves permanence. The views engine cannot host the
159 tail (views are durable documents; streaming through `view.update` would
160 bloat the log) β€” the tail is the app's own render over the ephemeral
161 stream, web-first.
162
163### 5. Event vocabulary
164
165Durable (thread log; the fold state every member converges on):
166
167| event | payload sketch | why durable |
168|---|---|---|
169| `session.spawn` | id, name, flavor, root/worktree, by | fleet board exists offline |
170| `session.status` | id, state (running/idle/waiting/dead), activity line | board truth |
171| `session.prompt` | id=target, text, by | the steering history IS the log |
172| `session.turn` | id, assistant text (final, per turn) | the conversation with the agent |
173| `session.result` | id, summary card (files changed, tests, links) | milestones as cards |
174| `session.approval` | id, request (tool, input digest), state, decided-by | audit trail of authority used |
175| `session.end` | id, reason, by | lifecycle honesty |
176
177Ephemeral (relay-only; live tail + presence):
178
179- `session.tool` β€” tool_use started/finished, truncated output chunks
180- `session.stream` β€” assistant text deltas while a turn is in flight
181- typing-style "session is thinking" indicators
182
183Streaming into *chat* has no partial-message primitive, so @claude's chat
184bubbles are **post-per-turn** (the durable `session.turn`), with the live
185delta visible in the drill-in tail instead β€” no post-then-edit churn in the
186feed.
187
188### 6. Approvals
189
190Claude Code's permission gate surfaces as a chip row β€” on the session's board
191and as a @claude chat card: "fix-login wants to run `git push` β€” [Allow]
192[Deny]". A tap emits an encrypted `session.approval` decision event; the
193bridge reads it and answers the gate. Flat authority: first decision wins,
194attributed in the event and shown in the feed ("Ana allowed `git push`").
195Notices/push ride the existing E2EE notify path, so "waiting for approval"
196reaches pockets.
197
198### 7. Liveness
199
200Derived, never declared (the docs/bots.md pattern): the bridge heartbeats
201(ephemeral presence + a periodic durable status touch); a session whose bridge
202is gone must **read as dead/stale** in the board β€” "last seen 4m ago" β€” not
203linger as "running". A dead bridge silently eating prompts is the worst
204failure mode; staleness is always rendered from event age, client-side.
205
206## What the server gains
207
208Nothing. Every piece rides existing surfaces: bot membership, thread events,
209ephemeral frames, the views engine, the mini-app framework, notify/push. The
210bridge is a new subcommand of the existing binary; the app is one new file
211under `web/static/apps/`.
212
213## Hard parts, named
214
215- **The approval round-trip** β€” latency between the gate firing and a human
216 tapping Allow; the session blocks meanwhile. Mitigations: push notices,
217 per-session auto-allow lists (later; config-side, never chat-grantable).
218- **Log discipline** β€” a busy session emits a lot; the durable/ephemeral split
219 above is the design's load-bearing wall. Anything high-churn must be
220 ephemeral or it bloats every member's encrypted log forever.
221- **Prompt-injection containment** β€” session output is model output over
222 untrusted repo/web content; it lands in chat as content, never as commands.
223 The bridge only ever acts on typed events from members.
224
225## Deferred (deliberately)
226
227- **Driver/observer roles** β€” layer later as `agents.role` fold state.
228- **Fleet-across-groups overview** β€” a "you"-placed app aggregating every
229 workstation group; needs nothing new from this design.
230- **Attaching to a human's interactive TUI session** β€” ruled out for v1
231 (tmux-scraping + keystroke injection fights the one-writer TUI; the honest
232 handoff is `--resume` after the bridge lets go).
233- **Auto-allow permission profiles** per session flavor.
234- **Native drill-in tail** β€” boards are native-free via the views engine;
235 the live tail ships web-first.
236
237## Decisions (Paul, 2026-08-28)
238
2391. **Naming: `agents` stands.** `lchat agents run`, `apps/agents.js`,
240 `session.*` events, and the member reads as @claude.
2412. **The prompt IS the chat message.** One `session.prompt` event, two
242 projections β€” the member's visible bubble in the feed and the bridge's
243 steering input, tagged with the target. No separate `chat.msg`.
2443. **Closing a session closes its worktree.** Sessions are closable from
245 within Eleven, and `session.end` is the cleanup moment: the bridge removes
246 a scoped session's worktree then. The branch (any committed work) survives
247 worktree removal as an ordinary git branch; only the checkout goes.
248