← Back to Articles & Artefacts
artefactswest

Stateloom live-canvas β€” the imitation blueprint

IAIP Research2026-08-01
ep304-living-review-design-260802-2ce1aed8-ba7c-4f93-af63-b90a630004ae

Stateloom live-canvas β€” the imitation blueprint

Provenance: produced 2026-08-01 by a read-only reverse-engineering agent dispatched by 🌿 Mino-Bimaadizi-Daa (day 09, session e9a78814) at William's direction: model on stateloom's infrastructure, do not consume its packages. Every claim carried file:line in the original run; this file is the seat's durable copy. Serves chart_1785632224671 / chart_1785632228800.


1. Transport β€” socket.io (engine.io v4: HTTP long-poll handshake β†’ WebSocket upgrade)

The live path is socket.io β€” not SSE, not polling, not file-watch-to-browser.

  • Hub server: bridge/src/hub.ts:98-102 β€” new Server(httpServer, { cors, serveClient: false }); binds at hub.ts:295.
  • Verified live: curl -s "http://127.0.0.1:4599/socket.io/?EIO=4&transport=polling" β†’ engine.io v4 handshake advertising websocket upgrade.
  • Browser client: bridge-client/src/client.ts:88-91.
  • Wire vocabulary, single source of truth: bridge-protocol/src/events.ts:46-64 (bridge:join, def:patch, def:full, def:request, def:ack, presence:*, bridge:error).

Two other mechanisms exist and are mistaken for the live path:

  • SSE is the legacy fallback (only when bridge URL env unset): web/src/components/DesignBridge.tsx:16-22, BridgeProvider.tsx:36 (EventSource("/api/watch")), app/api/watch/route.ts:63, driven by fs.watch at route.ts:24. Carries only an mtime ping β€” client re-GETs the whole file. Coarse reload, no animation.
  • chokidar file-watch lives inside the hub, not the browser: bridge/src/watcher.ts:48. Turns an external edit into granular ops: watcher.ts:67 diffDefinition(room.def, fileDef) β†’ watcher.ts:70-71 broadcast def:patch.

End-to-end trace: MCP add_state β†’ pixels

#WhereWhat
1mcp/src/server.ts:737readDef() β€” reads STATELOOM_PROJECT_FILE from disk
2mcp/src/server.ts:751mutate in memory
3mcp/src/server.ts:752 β†’ :115-117writeDef() β€” disk write FIRST
4mcp/src/server.ts:753-755 β†’ :213-224bridgeEmitPatch([{op:"state.add",…}]) stamped with statSync(PROJECT_FILE).mtimeMs
5bridge-client/src/client.ts:238-240socket.emit('def:patch', { docId, ops, origin: selfId, baseSeq, mtime })
6bridge/src/hub.ts:169-219dedup-ring check (:175) β†’ apply (:209) β†’ seq++ (:210) β†’ ring push (:211) β†’ broadcast to whole room incl. sender (:212-218) β†’ def:ack (:219)
7bridge/src/watcher.ts:63chokidar sees the same write; mtime/hash already in ring β†’ returns. No double-apply.
8bridge-client/src/client.ts:148-152gap check β†’ drop if origin === selfId β†’ fire('patch')
9bridge-react/src/session.ts:126-139split runtime vs structural ops; apply; commit() builds new immutable snapshot (:90-99)
10web/src/components/SocketBridgeProvider.tsx:70-74onPatch β†’ applyRemoteOps(...)
11web/src/store/useDesignerStore.ts:757-812_applyingRemote guard (:760), apply (:770), re-autoLayout (:782), preserve drill-down + selection (:787-800), zustand β†’ SVG re-render
12SocketBridgeProvider.tsx:86outbound subscription sees _applyingRemote β†’ does not echo back

React never polls: bridge-react/src/useSmcraftBridge.ts:56-60 β†’ useSyncExternalStore.

2. Truth & ownership

The .smdf.json file on disk is durable truth. The hub owns ordering only, never disk.

  • Invariant stated and enforced: hub.ts:1-12, docio.ts:1-6; no writeFile* anywhere in bridge/src.
  • Hub room = { docId, def, seq, mtime, ring, presence, watcher } (hub.ts:43-51), seeded from disk (hub.ts:109), snapshot to cold joiners (hub.ts:160).
  • Each writer persists through its own durable channel; the web app only writes disk on explicit πŸ’Ύ (Toolbar.tsx:62-67) β€” human edits live-but-not-durable, agent edits durable immediately.
  • docId = normalized absolute file path (docio.ts:51-53), one room per docId, same string env-injected into every process by scripts/live-loop.sh:24,36 (header documents the silent-divergence failure this prevents).
  • Schema: file top level { "stateMachine": … }; inner doc exactly settings, events, state (bridge-protocol/src/definition.ts:133-137).

3. Concurrency β€” last-write-wins, ordered by one event loop. No OT, no CRDT, no version check.

  • seq is a broadcast ordinal assigned solely by the hub, not persisted (hub restart resets to 0).
  • baseSeq on the wire is never validated by the hub β€” no optimistic concurrency exists.
  • Apply-throw β†’ bridge:error to sender + full-room snapshot resync (hub.ts:226-233). That is the whole conflict story.
  • Client gap detection: client.ts:122-128 (forward gaps only; a backward seq after hub restart is not detected).
  • The one real defense: dedup ring of last 20 {mtime, hash} per room (hub.ts:68-80), checked on both paths; correctness argued at watcher.ts:10-17 (both paths on one event loop).
  • Known UX regression: live mode silently clobbers unsaved local edits (useDesignerStore.ts:806 sets dirty:false), unlike the SSE fallback's "remote-changed" refusal.

4. Consuming AS-IS β€” footnote (per William's correction: we don't)

The protocol is state-machine-specific (ops.ts:22-44, SMDF definition.ts:133-137). The hub is ~90% document-agnostic (never reads a def field; SM coupling is three call sites: hub.ts:209, watcher.ts:67, unwrap at docio.ts:27/watcher.ts:40). AS-IS is not viable for a DPR model; the shape is the assignment.

5. Essential decisions (E1–E10) β€” these ARE the live quality

  • E1 Durable-first, then announce. Persist to your own store, THEN emit the exact op applied, stamped with the store's returned token. Never emit-then-persist: crash between = canvas ahead of truth, unrecoverable; reverse is merely stale and self-heals.
  • E2 The relay is a sequencer, not a database. In-memory mirror only for apply-without-round-trip and cold-joiner snapshots. No durability β†’ hub can die and restart without data loss.
  • E3 Granular ops on the wire; whole documents only for three cases β€” join, no-base recovery, post-error resync. Ops are why the canvas animates instead of re-mounting.
  • E4 One bidirectional channel, self-echo killed by origin. Broadcast includes the sender (so it learns its seq); client drops own echo by id; store marks remote-applied batches so outbound watcher doesn't re-emit. Without both guards: duplicate inserts, remove-throws on every self edit.
  • E5 Cold-load hydrates through the SAME callback as a live update. (session.ts:161-169; real production bug 46d5bc6: tab opened after the agent worked rendered an empty canvas.)
  • E6 Immutable snapshot + useSyncExternalStore. New snapshot object only when content changes. No polling, no context churn.
  • E7 Reconcile the out-of-band writer. Watch the durable store; diff oldβ†’new; broadcast the diff as ops; guard with a revision-token dedup ring so your own writers' persists don't double-apply.
  • E8 One canonical document id, computed absolutely, env-injected into every process.
  • E9 Fail open. A dead bridge never fails a tool (warn once; durable write already succeeded). The agent conversation never breaks because the canvas is down.
  • E10 Presence for free. A Map per room broadcast on join/leave β€” cheap, and most of the "this is alive" feeling.

Incidental (replace wholesale)

I1 the op vocabulary β†’ veritas writes element.add/update/remove, comparison.set, dominance.recompute; I2 diff/apply implementations β€” keep the contract apply(prev, diff(prev,next)) ≑ next with a roundtrip test, key by stable id; I3 the {stateMachine:…} wrapper and .smdf.json extension; I4 runtime.enter/exit β€” steal the idea (presentational highlight channel that doesn't mutate the doc), not the names; I5 all diagram geometry; I6 truth-as-a-file β€” veritas's truth is a Postgres row (see counter-argument).

The recipe (15 lines)

1  Standalone hub process (socket.io or ws). One room per documentId. Never writes durable truth.
2  documentId = one canonical string (veritas: model id), computed once, env-injected into every process.
3  Own protocol pkg, zero deps: DocType, Op union, pure apply(doc,ops), pure diff(prev,next), event name table,
   envelopes {docId, seq, ops|doc, origin, rev}. Ship a roundtrip test: apply(prev,diff(prev,next)) === next.
4  Hub room state: {doc, seq, revRing[20], presence Map}. join -> ack{selfId, snapshot, presence}.
5  Hub on op: revRing dedup -> apply -> seq++ -> ring.push -> broadcast to WHOLE room incl. sender -> ack sender.
   On apply throw: error to sender + full-doc resync to room.
6  Every writer (MCP tool, CLI, web action): persist to Postgres FIRST, then emit that same op stamped with
   the returned revision/updated_at. Failure to emit = warn once, never fail the tool.
7  Client wrapper: join; on inbound seq > lastSeq+1 -> request(sinceSeq) resync; drop frames where origin===selfId;
   expose patch/full/presence/status callbacks. NOTE: also re-emit join on socket 'connect' (stateloom omits this).
8  React binding: session holds an immutable snapshot, commit() on change, subscribe -> useSyncExternalStore.
9  Fan the join-ack document out through the same onFull callback as a live update (cold-load hydration).
10 App store: applyRemoteOps(ops) behind an _applyingRemote flag; recompute derived layout; preserve selection
   and drill-down across syncs so the view doesn't snap on every agent tool call.
11 Outbound: subscribe to the store, skip _applyingRemote batches, debounce ~60ms, emit diff(lastSynced, current).
12 Out-of-band writers: Postgres LISTEN/NOTIFY (or skip if all writes go through your API) -> diff -> broadcast.
13 Presence: role + name + deterministic color from clientId; broadcast the list on join/leave.
14 A launcher script that resolves and exports every shared value once (id, hub url, ports) β€” for hub, MCP and web.
15 Optional handshake token; CORS on the hub only when a browser connects to it.

Strongest argument AGAINST reusing stateloom's bridge (why we imitate instead)

Its correctness is built on a local file with an mtime, and veritas has neither a file nor a place to run a stateful singleton. Every defense keys on statSync(...).mtimeMs; veritas's truth is a Neon Postgres row written by serverless route handlers β€” no mtime, nothing for chokidar to watch, no guarantee the writing process is the emitting process. A substitute revision token must be invented and proven under concurrent serverless writers, not one Node event loop. The hub is stateful, single-instance, localhost-bound, with the socket URL baked into the client bundle at build time. And the survivable-on-localhost defect that isn't survivable hosted: no rejoin on reconnect β€” after any hub restart/blip the client is connected-but-in-no-room, patches silently dropped at hub.ts:171, canvas quietly stale. reconnect appears nowhere in the bridge sources except a test disabling it.

Model the shape. Don't inherit the file.