"Finn? Talks to the dead, mostly." — Neuromancer, William Gibson
A multi-agent chat UI with human-in-the-loop approval. One window, many agents (LLM endpoints, agent frameworks, anything that speaks HTTP), and a human routing every cross-agent message by hand. Named after Gibson's Finn — the fixer who routes between the living and the ROM-stored dead.
Status: working spike, with day-to-day usable surface area. Single- and two-machine setups verified end-to-end. The application is structurally split into two surfaces:
- Channel view (
/) — conversational. Per-channel chat with KB-budgeted initial load, mention autocomplete, approval flow for cross-agent traffic, in-browser CRUD for channels and agents, and user-controlled grooming. - Protocol viewer (
/protocol) — audit. Cross-channel browse, search, filter, and markdown export of the full message history including groomed rows.
All phase 1 (daily-use blocker) issues are closed: token-streaming
with reply-sequencing (ADR-0013), per-message token-usage display
(#43), markdown rich-rendering with mention spans and the
ResizeObserver scroll discipline (ADR-0016 / #1). Manual message
forwarding (ADR-0014) is live. Settings surface, UI refresh, top-nav, per-channel drafts, memory export, CSP headers, and markdown composer all shipped. See §"Roadmap" for current open work. Wintermute and any other
OpenAI-compatible backend are reachable via the
openai-compatible connector type.
A web UI for chatting with several agents from one place. Each channel can hold one or more agents; the user is always present. When two or more agents share a channel, every cross-agent message goes through an explicit Approve / Reject step at the UI, so the human is structurally always in the routing loop.
It is not:
- an agent itself (no LLM, no tools, no memory of its own beyond a transcript log)
- a generalised messaging platform (single-user, internal use only)
- tied to any particular agent stack (OpenClaw, Wintermute, raw LLM APIs are all just connectors)
Sitting at one keyboard and switching between five chat surfaces — OpenClaw on the Mac host, OpenClaw remote, OpenClaw in a VM, Wintermute on a server, raw API calls to Claude / Moonshot / etc. — is the daily friction this tool removes. As a side effect, it lets two or more agents "talk" via the user as relay, with the conversation visible to all parties and logged centrally.
The structural payoff: the same human-in-the-loop pattern that prevents inter-agent feedback loops from spiralling stays built into the medium. There is no agent-to-agent direct channel; routing is the user's job, mediated by the UI.
┌────────────────────────────────────────────────────────────────────┐
│ Browser │
│ / │
│ src/routes/+page.svelte ← channel view │
│ src/lib/ui/{MessageBubble, Modal, │
│ ChannelForm, AgentForm, │
│ MentionPopup}.svelte │
│ /protocol │
│ src/routes/protocol/+page.svelte ← audit surface │
└──────────────────┬─────────────────────────────────────────────────┘
│
│ HTTP REST WebSocket /ws
│ /api/channels, /api/agents • chat events
│ /api/messages/:id/visibility • approval events
│ /api/protocol, .../export • state_changed
▼
┌────────────────────────────────────────────────────────────────────┐
│ finn server (SvelteKit + Node) │
│ │
│ ┌────────────────────┐ ┌────────────────┐ ┌──────────────────┐ │
│ │ src/routes/api/ │ │ attach.ts │ │ hooks (per turn)│ │
│ │ REST writes │◀─│ WS broadcast │◀─│ user_message │ │
│ │ zod validation │ │ globalThis │ │ approval_decide │ │
│ └─────────┬──────────┘ └────────────────┘ └─────────┬────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ core engine │ │
│ │ • messages.ts append-only writers + scope=all/chan │ │
│ │ • approvals.ts state machine │ │
│ │ • mentions.ts @-parser, channel-scoped resolve │ │
│ │ • channel-agent.ts per-channel agent lookup │ │
│ │ • protocol.ts cross-channel filter + cursor pagin. │ │
│ │ • export-channel.ts per-channel + cross-channel markdown │ │
│ │ • connectors/registry.ts │ │
│ └─────────────────────────────┬────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────┴────────────────────────────────┐ │
│ │ connectors │ │
│ │ • openclaw.ts OpenClaw Gateway w/ scopes/sess-key │ │
│ │ • openai-compatible.ts vanilla OpenAI Chat Completions │ │
│ │ • anthropic-stub.ts canned replies, dev/test │ │
│ │ • sse-parser.ts shared SSE-frame consumer (PR #39) │ │
│ │ • (planned) anthropic.ts — direct, bypassing OpenClaw │ │
│ └─────────────────────────────┬────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────┴────────────────────────────────┐ │
│ │ persistence │ │
│ │ • db/{schema, client, ids, agent-config}.ts │ │
│ │ • SQLite via Drizzle ORM → ~/finn-data/finn.db │ │
│ └──────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────┐
│ agent endpoints (out-of-process, HTTP) │
│ • OpenClaw Gateway scoped operator headers (ADR-0001) │
│ • Wintermute /v1/* OpenAI-compat, bearer-gated, TLS │
│ • Other OpenAI-compat Open WebUI, vLLM, llama.cpp, … │
│ • Anthropic API direct planned │
└────────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────────┐
│ ~/finn-data/ │
│ finn.db SQLite database (managed by Drizzle) │
│ secrets/.env bearer tokens; chmod 600 │
│ exports/ markdown channel exports (planned) │
└────────────────────────────────────────────────────────────────────┘
- Frontend + backend in one repo: SvelteKit (single deployable, BFF pattern matches the use case).
- Persistence: SQLite via Drizzle ORM. File-based, no extra service. Migrate to Postgres later if the log volume warrants it.
- Real-time: WebSocket between browser and server, with streaming hooks — broadcasts go out as soon as each piece is ready, so the user's own bubble appears immediately and connector latency does not delay it.
- Validation: Zod for connector configs (discriminated union per
connector_type).
- SvelteKit over a Python stack: finn is a frontend-shaped problem; Python's strengths are wasted here, JavaScript's WebSocket and DOM stories are not.
- SQLite over Postgres / Qdrant: the log is append-only structured data, no semantic search, no high concurrency. SQLite is the right size.
- WebSocket only between browser and server: connectors target agents that may speak only HTTP. Lifting the WebSocket requirement to the server boundary keeps every connector independently implementable.
- Application-layer message immutability (ADR-0004): no DELETE path in code; the chat log is the audit log.
- Application-generated IDs (ADR-0003): prefixed nanoid-12 ids read well in URLs, headers, and human conversation.
Five tables. The first four are the core; the fifth (approvals) is
what makes the human-in-the-loop pattern explicit in the schema.
agents -- configured endpoints
id, name, connector_type, config (json), enabled,
created_at, deleted_at -- soft-delete (ADR-0004)
channels -- rooms with 1..N agents
id, name, description,
created_at, deleted_at -- soft-delete (ADR-0004)
channel_members -- which agents are in which channel
channel_id, agent_id, joined_at -- hard-delete (ADR-0004)
messages -- everything that's been written
id, channel_id, sender_type (user|agent|system), sender_id,
body, created_at, parent_message_id,
hidden_at, hidden_by -- visibility marker (ADR-0004 addendum)
-- content immutable; visibility mutable
approvals -- the human-in-the-loop step (ADR-0005)
id, message_id,
status (pending|approved|rejected|routed),
targeted_agent_ids (JSON array),
reject_reason (nullable),
created_at, decided_at -- append-only (ADR-0004)A separate audit_log table is not needed: messages joined with
approvals already records every byte sent, every routing decision,
every reject reason, with timestamps. Markdown export is a SELECT.
See docs/decisions/0003-id-formats.md
for the ID format and
docs/decisions/0004-message-persistence.md
for the per-table delete policies.
State machine (ADR-0005):
pending ──approve──▶ approved ──relay done──▶ routed
│
└──reject──▶ rejected
Triggers:
- User → agent: no approval (the user is the human-in-the-loop; their message is already decided).
- Agent → user (no
@-mentionto other agents): no approval. - Agent → agent (any
@-mentionof another channel member): one approval row per such message, statuspending. - User-triggered forward (user clicks ↗ on an existing
bubble): the picked targets receive the body verbatim. The
user's deliberate click is the human-in-the-loop step; the
approval row lands directly in
routedstatus. ADR-0014 has the full rationale (forwarding is a second legitimate routing shape, not a bypass of the gate).
Each agent message bubble carries its approval state inline: status
badge, target picker (pre-filled from @-mentions, user-overridable),
Approve / Reject buttons, optional reject reason. There is no
separate approval inbox — the message is the unit of decision.
Full rationale, sender experience, recursive approval semantics, and
wire protocol: docs/decisions/0005-approval-flow.md;
forwarding details in docs/decisions/0014-user-triggered-forwarding.md.
- 1:1 channel (user + one agent): the user's message goes to the agent. The agent replies. No approvals.
- Multi-agent channel (user + N agents): the user's message
fans out to all agents in parallel (no approval). Each agent's
reply that mentions another channel member produces a
pendingapproval; the user can approve, modify the target set, or reject.
Mentions are convenience for pre-filling the target picker. The user's choice in the approval UI is what actually routes.
Inbound (client → server):
{ type: 'user_message', channel_id, body }
{ type: 'approval_decide', approval_id, decision: 'approve'|'reject',
targets?, reject_reason? }
{ type: 'forward_message', message_id, target_agent_ids: string[] }
{ type: 'ping' }Outbound (server → client, streamed via per-event broadcast):
// User and system messages — single event, body arrives whole.
{ type: 'message', channel_id, sender, sender_id, body, ts, id }
// Agent replies — four-event streaming lifecycle (ADR-0013).
{ type: 'message_start', id, channel_id, sender_id, ts }
{ type: 'message_delta', id, delta }
{ type: 'message_end', id, body, tokens? } // tokens optional, issue #43
{ type: 'message_error', id, error } // mutually exclusive with message_end
// Approval lifecycle.
{ type: 'approval_created', approval, message_id } // for mention-approvals AND forwards
{ type: 'approval_updated', approval }
// Domain CRUD echo.
{ type: 'state_changed', entity: 'channel'|'agent'|'channel_member'|'message',
action: 'created'|'updated'|'deleted',
id, extra? }
// Misc.
{ type: 'system', body }
{ type: 'pong' }state_changed events with entity: 'message' carry the
channel id and the new hidden boolean in extra, so connected
clients can update visibility without a full refetch. The
protocol viewer at /protocol does not subscribe to live events
— it is a snapshot surface (ADR-0010 §5).
User-message message events arrive as soon as each one is
persisted, so the user's own bubble appears in milliseconds.
Agent replies stream as message_start → N × message_delta
→ message_end (or message_error) so the channel sees per-
recipient bubbles fill at their own pace, and the slowest agent
never blocks the rest. state_changed notifies connected
clients of CRUD changes (a channel renamed in one tab is reflected
in another within a roundtrip). See src/lib/server/ws/attach.ts
for the canonical schema.
Read:
GET /api/channels list active channels
GET /api/channels/:id/messages message history
(?limit=&before= | ?budget=<kb>)
GET /api/channels/:id/search?q= substring search in channel
GET /api/channels/:id/export?format=md|memory single-channel markdown or memory-log download
(?since=ISO&until=ISO for memory format)
GET /api/channels/:id/members channel members
GET /api/channels/:id/approvals approval state hydration
GET /api/agents list active agents
(?include_archived=1)
GET /api/agents/:id single (with parsed config)
GET /api/protocol cross-channel audit query
(filters: channels=&q=&sender_types=&
senders=&from=&to=&visibility=&
only_rejected=&cursor=&limit=)
GET /api/protocol/export?format=md|memory cross-channel markdown or memory-log download
(same filter params)
Write:
GET /api/settings global settings
PATCH /api/settings save global settings
GET /api/settings?channelId=<id> per-channel settings
PATCH /api/settings/channel/<id> save per-channel override
DELETE /api/settings/channel/<id> reset to global
DELETE /api/inflight-messages/<id> dismiss inflight bubble (PR #120)
The channel messages endpoint has two modes: limit+before for
'load older' pagination, or budget=<kb> for the KB-bounded initial
load (ADR-0011). The protocol endpoints share the same filter
vocabulary; pagination there is cursor-based on (created_at, id)
(ADR-0010). Both export endpoints set Content-Disposition: attachment so the browser saves the file.
Write (all bodies validated by zod; all writes also broadcast a
state_changed WS event):
POST /api/channels create
PATCH /api/channels/:id rename / re-describe
DELETE /api/channels/:id soft-delete (Archive)
POST /api/channels/:id/members add member
DELETE /api/channels/:id/members/:agentId remove member
POST /api/agents create
PATCH /api/agents/:id name / enabled / config
DELETE /api/agents/:id soft-delete (Archive)
PATCH /api/messages/:id/visibility groom: hide / unhide
connector_type is locked at agent creation; PATCH ignores any
attempt to change it (ADR-0007 §"Decision 3").
Message visibility is the one allowed mutation on the messages
table. Body, sender, and timestamp remain immutable; only
hidden_at and hidden_by flip on grooming. See ADR-0004's
2026-05-07 addendum for the 'immutable but extendable' discipline.
In ascending order of integration weight:
- One-on-one chat ✓ — user ↔ agent over OpenClaw connector, per-channel session continuity (ADR-0002 + 0012).
- OpenAI-compatible connector ✓ — talks to any backend that
speaks vanilla OpenAI Chat Completions. Verified end-to-end
against Wintermute's
/v1/*adapter atagent.storm7.de. Seedocs/connectors.mdScenario C. - Anthropic connector — stub only today (canned replies for
exercising the multi-agent flow without a real key). Anthropic
Cloud is otherwise reachable via the
openclawconnector when OpenClaw is configured for it; a direct connector that bypasses OpenClaw is on the roadmap. - Multi-agent channel ✓ — user, OpenClaw, OpenAI-compat
(Wintermute), and/or stub agents in one room.
@-mentions, targeted approval, recursive approval for relayed replies that mention yet another agent. - Channel + agent CRUD UI ✓ — in-browser create / edit /
disable / archive via modal forms. Channel-create and -edit
member selection uses an email-client-style chip input
(filter-as-you-type, Backspace-to-remove, ARIA combobox).
Live cross-tab sync via
state_changedWS events. ADR-0007; chip-input PR #61. - Log surface ✓ — backwards pagination ('Load older'), per-channel substring search, sender / system / rejected- full-channel markdown export as browser download, memory-log export for agent daily notes. ADR-0009, #117.
- Mention autocomplete ✓ — typing
@in the composer pops up channel-member candidates, keyboard-navigable. ADR-0009 §5/6. - User-mention dispatch filtering ✓ —
@gwen hiin a multi-agent channel only dispatches to Gwen, not to every channel member. Closed by PR #29 (issue #27). - KB-budget initial load ✓ — channel view caps cumulative body size on first paint (default 200 KB). 'Load older' still walks back further. ADR-0011.
- Channel grooming ✓ — hide-from-channel-view marker on each message bubble; protocol viewer and exports ignore the marker per audit discipline. ADR-0004 addendum.
- Protocol viewer ✓ — separate
/protocolroute. Cross- channel browse with multi-channel filter, full-text search, sender filter (type + specific agent), date range, visibility selector, only-rejected flag, cursor-paginated, markdown export of the current filter result. URL search-params are the filter source-of-truth. ADR-0010. - End-to-end token streaming ✓ — the dispatcher fans out
via
streamUserMessage/streamToAgent, both driving the per-agentstreamOneAgentcore that emitsmessage_start/delta/end(orerror) over the WebSocket. SSE parsing is insse-parser.ts; per-message token usage is captured from the upstreamusageblock when the backend reports one and persisted asmessages.tokens_json. ADR-0013- issue #43.
- User-triggered forwarding ✓ — ↗ in a bubble's hover
toolbar relays the body verbatim to picked channel members,
landing directly in
routedstatus; the user's deliberate click is the human-in-the-loop step. ADR-0014. - Streaming status icon + token-count footer in bubbles ✓
— ● streaming, ✓ done, ⚠ errored in the header;
tokens: total (↓input, ↑output)in the footer when the backend reports usage,tokens: —with tooltip otherwise. Always-on for agent bubbles for layout consistency. Issue #43. - Rich-rendered message bodies ✓ — GFM markdown via
marked+DOMPurify, soft line breaks, fenced code blocks (no syntax highlighter — deliberate, ADR-0016 out of scope), inlinecode, lists, tables, blockquotes.@-mentionpost-process renders styled spans only when the token resolves against current channel members; skips text inside code blocks and existing links. Same pipeline for user and agent bubbles — the sanitiser is the safety control, not the source. Streaming bubbles render plain text + cursor; the markdown finalisation triggers onmessage_end. - ResizeObserver scroll discipline ✓ — one observer on
the messages-container catches every layout-changing
event (streaming deltas,
message_endmarkdown finalisation, lateapproval_createdadding buttons, forward picker expanding mid-channel). Snaps to bottom when the user is at-or-near the bottom (50 px threshold); leaves the user alone when they scrolled up to read history. Replaces the per-event scroll triggers from earlier PRs. ADR-0016 §8. - Mermaid diagrams in bubbles ✓ — fenced code blocks
tagged
mermaidrender as SVG diagrams instead of plain monospace source. Three-layer sanitiser (label pre-escape, MermaidsecurityLevel: 'strict'withhtmlLabels: false, post-render DOMPurify with an explicit SVG allowlist). Mermaid bundle is dynamic-imported on first diagram mount, so channels that never see a diagram pay zero bundle cost. In-memory SVG cache keyed(source, theme, mermaidVersion). Plain-while-streaming — the SVG render lands atmessage_endwith a 150 ms fade transition. Parse / render failures fall back to a monospace source block with a small inline error caption. ADR-0022, issue #80. - Image rendering in bubbles ✓ — HTTPS image URLs render inline.
Scheme-gated (HTTPS only), DOMPurify post-pass, max-width
constrained, graceful error fallback. CSP
img-src https:keeps cross-origin image loads under policy. ADR-0023. - Top-navigation layout ✓ — sidebar removed; full-width chat. Top nav: brand + channel dropdown, search + export + filter pills (system/rejected/groomed), protocol link + settings gear. Agent toggle bar below nav: per-channel agent chips with enable/disable toggle. PRs #147, #154, #155.
- Settings surface ✓ —
/settingswith Global/Agents/Channels rail. Inline expand/collapse per agent card (no modal); inline per-channel settings; channel name/description edited in-place. Theme picker, KB budget + roundtrip cap overrides, auto-approve per channel. ADR-0019, PRs #148/#149/#154–#169. - Per-channel composer draft ✓ — switching channels preserves in-progress messages; cleared on send. PR #121.
- Protocol viewer markdown rendering ✓ — messages render via
the same
renderMarkdown()pipeline as chat bubbles. Raw/rendered toggle in the sidebar. Memory-log export (format=memory) produces a structured markdown file suitable asmemory/YYYY-MM-DD.md. PRs #125, #126. - Content Security Policy ✓ —
script-src 'nonce-{n}',frame-ancestors 'none',object-src 'none',img-src https: data:, plusX-Content-Type-Options,X-Frame-Options,Referrer-Policy,Permissions-Policy. Inter font self-hosted (no CDN). PR #124. - Markdown composer ✓ — chat input is a plain textarea with
Bold / Italic / Code / Link toolbar buttons that insert markdown
syntax. Keyboard shortcuts Ctrl+B/I/E. Auto-expands up to 10 rem.
MarkdownComposer.svelte, PR #177.
- Authentication. Single-user local tool. Adding auth is its own ADR (planned for when finn moves off the Mac+VM trust domain).
- Agent-to-agent direct connectors. Everything goes through the UI. By design.
- Files, images, voice. Future ADRs.
- Cross-channel search beyond the protocol viewer. Today's
search lives at
/protocol; an integrated cross-channel search in the conversational view is a follow-up under the same surface.
~/Repositories/finn/ # repo
~/finn-data/ # persistent state, never touched by the repo
├─ finn.db # SQLite database
├─ finn.db-wal # SQLite WAL, present at runtime
├─ finn.db-shm # SQLite shared memory
├─ secrets/
│ └─ .env # bearer tokens (chmod 600)
└─ exports/ # markdown exports for NotebookLM, etc. (planned)
The repo never writes to ~/finn-data/. Database file stays out of
the repo. Secrets stay out of the repo. Exports stay out of the repo.
Two full setup guides, depending on where the OpenClaw gateway runs:
docs/setup.md— finn and gateway on the same host (single-machine). Loopback-only.docs/setup-mac.md— finn on macOS, gateway in a UTM VM (two-machine). Adds a network path between the two; raises the urgency of the trust-mode migration documented in ADR-0001 §"Addendum 2026-05-07".
TL;DR for the single-machine variant:
npm install
mkdir -p ~/finn-data/secrets
echo 'FINN_OPENCLAW_API_KEY=<your-gateway-token>' > ~/finn-data/secrets/.env
chmod 600 ~/finn-data/secrets/.env
npm run db:migrate
npm run db:seed
npm run devFor Mac+VM, the env file additionally needs FINN_OPENCLAW_BASE_URL
pointing at the VM gateway, and the VM must have its gateway bind
switched from loopback to lan. Full walkthrough in
docs/setup-mac.md.
finn is a scoped operator UI, not a sandboxed application. It is for the same human who operates the connected agents, and it talks to those agents over an authenticated channel.
The target authentication architecture:
- Each connected OpenClaw Gateway runs in
trusted-proxymode, with the network source (tailscale, loopback, or equivalent) as the trust boundary. - finn always sends
x-openclaw-scopes: operator.read operator.writeon every request. Admin, approvals, pairing, and secrets-talking scopes are explicitly not requested. - A finn process compromised at the application layer therefore
cannot reconfigure a gateway, manipulate the approval queue, pair
new devices, or reach the secrets-talking surface — it is bounded
by what
operator.read+operator.writeallow. - The bearer secret lives at
~/finn-data/secrets/.envwith0600permissions, outside the repository.
In the current transitional posture, gateways may still run in
token mode, in which case the scope header is ignored and finn
behaves as a full operator (same trust posture as the OpenClaw TUI on
the same host). The code is already written for the target posture;
the migration is a gateway-config task, not a finn change.
Full rationale, options considered, transitional posture, and
migration sketch:
docs/decisions/0001-openclaw-connector-auth.md.
- storm7. This project has nothing to do with the storm7.de stack. No subdomain, no shared infrastructure, no overlap.
- MCP / tool-use. finn is a chat router, not an agent. Agents may have their own tools (and many will), but finn neither knows nor cares.
- Agent memory. The transcript log is the user's logbook, read by humans, not by agents. Connectors do not query it.
Issues are tagged with phase 1 / phase 2 / phase 3 labels
reflecting how directly they affect daily-use viability. Phase 1
issues are what stand between the current spike and finn being a
tool you reach for every day.
Phase 1 — daily-use blockers: all closed. This phase represented what stood between the spike and finn being a tool you reach for every day. Marker reached on 2026-05-09. See §"Closed since the last roadmap refresh" below for the trail.
Phase 2 — quality-of-life: substantially complete.
- #18 Settings surface ✅ — shipped across PRs #148–#169. Global defaults, per-channel overrides, inline agent/channel management, theme picker, auto-approve toggle.
- #26 Channel-create member selection ✅ — PR #61.
- #28 Auto-approve agent mentions ✅ — ADR-0015, shipped.
- #106 CSP headers ✅ — PR #124.
- #114 Per-channel draft ✅ — PR #121.
- #117 Memory-log export ✅ — PR #125.
- #147 Top-nav + sidebar removal ✅ — PR #156.
Open work:
- #6 Discovery: session memory (finn ↔ agent ↔ user). Parked; #117 memory export is the first concrete step.
- #30 Protocol viewer: archived channels missing from channel filter. Parked until deployment (#110) is settled.
- #46 Multi-User with SSO. Long-term; parked.
- #105 Image paste/upload in composer — nice-to-have.
- #110 Containerised deployment + reverse-proxy auth. Actively needed for hoschis-pit.
- Markdown composer toolbar polish (#177 shipped v1; button placement + styling follow-up pending).
Unphased / follow-up:
- SQLite FTS5 / ranked search when LIKE feels slow.
- Range-select mark-and-export of a channel slice.
- Date-jumper / calendar pagination for very long channels.
- Server-side
~/finn-data/exports/write alongside the browser download. - Syntax highlighting in fenced code blocks (ADR-0016 §out-of-scope).
- Real Anthropic connector (replaces the stub).
- WYSIWYG markdown input (TipTap) — deferred, UX decision pending.
Closed since last roadmap refresh (2026-05-08 – 2026-06-12):
Phase 1 (daily-use blockers) — all closed 2026-05-09:
- #3 Streaming + sequencing (ADR-0013, PRs #39–#47)
- #43 Token-usage display (PRs #44, #50, #51, #58)
- #1 Rich-rendering (ADR-0016, PR #58)
- #52 Manual forwarding (ADR-0014, PRs #53, #54)
- #26 Channel-create chip input (PR #61)
Phase 2 + quality-of-life — 2026-05-09 to 2026-06-12:
- #18 Settings surface (PRs #148–#169)
- #25 Channel name reuse after archive (PR #123)
- #28 Auto-approve channels (ADR-0015, shipped)
- #80 Mermaid rendering (ADR-0022)
- #106 CSP headers (PR #124)
- #114 Per-channel draft (PR #121)
- #115 Email addresses swallowing messages (PR #119)
- #117 Memory-log export (PR #125)
- #118 Inflight bubble ordering + dismiss (PR #120)
- #129–#147 UI refresh: warp-style tokens, top-nav, agent toggle bar, sidebar removal (PRs #142–#156)
- #148/#154 Settings: agent + channel management (PRs #149–#155)
- #22 Closed — no demand (connector model override)
- Approval-recovery on server restart while a row is
approved. - Tests (unit + integration; current debt).
launchdplist fornpm run startonce the spike stabilises.- OpenClaw gateway auth-mode migration to
trusted-proxyfor cross-machine deployments (see ADR-0001 addendum).
| File | Purpose |
|---|---|
README.md (this file) |
front door, capabilities, links |
docs/contributing.md |
branch / PR workflow (ADR-0006) |
docs/setup.md |
single-machine setup (gateway local) |
docs/setup-mac.md |
two-machine setup (gateway in VM) |
docs/decisions/ |
ADRs — architectural decisions, immutable + numbered |
docs/lessons.md |
mistakes made, fixes shipped, things to remember |
src/lib/server/README.md |
the two-build convention for server modules |
This README, the architecture sketch, and the implementation are a
collaboration between Jürgen (project owner) and Dixie (sister-agent
of the OpenClaw-on-VM instance), starting 2026-05-06. The
conversation that shaped the design happened in those daily
sessions; the design decisions are pinned in docs/decisions/ so
they are not lost when sessions reset.
Commits are authored as Dixie with Co-authored-by for Jürgen so
GitHub provenance reflects both.
juergenvh/wintermute— personal AI agent (Python, on Hetzner). Will get a finn-connector eventually but is not part of this stack.juergenvh/openclaw-server— storm7 deployment configs. Unrelated to finn; mentioned only so it's clear they don't share anything.juergenvh/agenticframework— the universal meta-framework. Also unrelated; finn is a tool, not a phase.