WebSocket & actors
Gateway join, presence, deck.line / deck.block.
This document specifies how actors (browser, worker) connect to a session gateway, exchange deck and direction text, and how workers persist memory in SQLite + vectors.
1. Session gateway
1.1 Transport
- URL:
ws://<host>:<port>(e.g. ws://127.0.0.1:35987). No path; first message from client must bejoinand must includesessionId. - Framing: each message is a JSON object (UTF-8 text frame). For high-volume token streams, clients may batch; gateway may reject frames > 256 KiB.
1.2 Join handshake
First message from client must be join (must include sessionId and actorId):
{
"type": "join",
"sessionId": "default",
"actorId": "actor-uuid-123",
"channelIds": ["default"],
"skillIds": ["add_track", "adjust_instrument", "pattern_steps", "pattern_piano", "channel_mix", "master_mixer"]
}
Gateway responds:
{
"type": "joined",
"sessionId": "...",
"you": { "actorId": "...", "clientId": "...", "channelIds": ["default"] },
"actors": ["actor-1", "actor-xyz"],
"replay": []
}
actors: list of actorIds in the channel.actorId(required): for human joins, the gateway picks a paired agent lane when exactly one agent is online (v1 prefersai-a); otherwisenull. Browsers show “No agent” until an agent connects.- When anyone joins or leaves, the gateway broadcasts
presence:{ "type": "presence", "sessionId": "...", "actors": ["agent-1", "human-xyz"] }.
Human deck stream (Play)
While the browser actor is playing, the browser sends:
control{ "type": "control", "op": "playing_start", "actorId": "...", "authorId": "...", "perfStep": <host 16th> }— agents mark the session live and may run inference after buffered deck arrives.deck.lineper emitted deck line (throttled). The gateway stampsactorIdfrom the connection and fans out.control{ "op": "playing_stop", ... }on stop — agents clear live mode.
Agents append deck.line text from the playing actor to a rolling buffer and respond with deck.stream_chunk then deck.block with their own actorId.
1.3 Message families
type | Direction | Fields |
|---|---|---|
deck.line | any → gateway → fanout | actorId, line, authorId, seq (hub-assigned) |
deck.block | any → gateway | actorId, lines[], authorId, optional effectivePerfStep, submitDeadlinePerfStep, asap — see STREAM_PROTOCOL.md |
deck.stream_chunk | worker → gateway → browsers | actorId, chunk, authorId (no seq until line commit) |
direct | any → gateway → target actor | targetActorId (target), text, authorId, optional perfStep (host 16th index when sent) |
state.snapshot | gateway or browser | hash, tplPreview (truncated), ts |
control | master | op, payload — see STREAM_PROTOCOL.md |
error | gateway → client | code, message |
Error codes actually emitted by services/gateway/main.tish: BAD_JSON, JOIN_BAD, JOIN_FIRST. Codes such as SKILL_DENIED, LEASE_CONFLICT, PARSE_FAIL, RATE_LIMIT, and AUTH are aspirational — skill / ownership violations are currently handled by the receiver silently skipping disallowed lines (see DJ_SKILLS.md).
1.4 Ordering
- Gateway maintains
seqperactorId(monotonic). Alldeck.line/deck.block/directget a server timestamp + actor seq. - Cross-lane order is not total; merge rules live in the browser (CO_DJ_SPACE.md).
1.5 Rooms
- One room per
sessionId. All joined clients receive fanout for messages they are allowed to see (v1: fanout all).
2. Worker contract (actor + skills)
2.1 Process model
- One worker per actor per session (e.g.
actor-1), or a pool with one job per(sessionId, actorId). - Worker connects with
actorId(e.g.actor-1),channelIds: ["default"],skillIds: [...].
2.2 Input loop
The persistence / RAG steps below (local DB, embeddings, chunks) are planned — see §3. Today the worker keeps an in-memory rolling context buffer only.
- On each inbound
deck.line/deck.block(other actors or merged snapshot): append to the context buffer (planned: also persist to local DB and embed chunks). - On each
directwheretargetActorIdmatches this actor'sactorId: enqueue for response. - On timer or debounce: build context = system prompt + last N buffered messages (planned: + RAG top-k from
chunks).
2.3 Output loop
The worker (services/agent-worker/main.tish) calls its LLM: handlePlayingStream and handleDirect snapshot the buffered peer stream into one prompt (llmLinesFrom), call callLLM, and stream the real model output — first as deck.stream_chunk (the finished text char-by-char), then as a committed deck.block. It falls back to a demo patch only when GRADIENT_MODEL_ACCESS_KEY (or MODEL_ACCESS_KEY) is unset or the call fails. The worker also ingests inbound deck.block from other actors into its context buffer so responses are aware of them. The old 3-second periodic demo deck.block flood has been removed.
Note: the worker streams the finished text char-by-char — provider-side SSE token streaming (live provider tokens) is planned, not implemented.
- Snapshot the buffered peer stream into one prompt; call
callLLMand stream the resulting text. - Emit
deck.stream_chunk(per character of the finished output) for the UI typing indicator, then adeck.block. - Validate deck line against actor's skills (DJ_SKILLS.md); if invalid, log and skip or send
errorto gateway. - For
deck.block: seteffectivePerfStepto hostperfStep+ lookahead (default 64 sixteenths ≈ four 4/4 bars). SetsubmitDeadlinePerfStepto host step + slack (e.g. 48) so the host drops the block if it arrives too late. Omit both and useasap: truefor emergency edits. Alternatively put@ perf_step Nas the first line of deck (parsed by host; same aseffectivePerfStep).
2.4 Reconnect
- Resend
joinwith sameactorId,channelIds,skillIds; gateway sendsreplay. - Planned / not implemented in
services/agent-worker: loading state from a SQLitemessagestable on reconnect. The worker is in-memory only — its context buffer does not survive a restart.
3. SQLite + vector schema (Planned)
Planned / not implemented in
services/agent-worker. The worker is currently in-memory only: it holds a rolling context buffer in process and does not open SQLite, embed chunks, or run vector/RAG retrieval. The schema below is a forward-looking spec for agent memory, kept here as the target design.
Path: e.g. ~/.deckard-sessions/<sessionId>.sqlite (agent-local) or shared if gateway persists.
3.1 Tables (SQL)
CREATE TABLE messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
actor_id TEXT NOT NULL,
role TEXT NOT NULL, -- tpl | direct | system | tpl_out
body TEXT NOT NULL,
hub_seq INTEGER,
ts INTEGER NOT NULL
);
CREATE TABLE tpl_snapshots (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
actor_id TEXT NOT NULL,
tpl_text TEXT NOT NULL,
ts INTEGER NOT NULL
);
CREATE TABLE chunks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
source TEXT NOT NULL, -- tpl | direct | doc
text TEXT NOT NULL,
ts INTEGER NOT NULL
);
-- Embedding storage: use sqlite-vec / sqlite-vss extension:
-- CREATE VIRTUAL TABLE chunk_embeddings USING vec0(
-- chunk_id INTEGER,
-- embedding FLOAT[1536] -- dimension matches model
-- );
CREATE TABLE agent_memory (
session_id TEXT PRIMARY KEY,
summary TEXT,
updated_ts INTEGER
);
3.2 Vector index
- sqlite-vec or sqlite-vss: store
chunk_id+ embedding; on ingest, chunk deck lines (~512 chars) anddirectmessages; query with same embedding model as ingest. - Fallback: no extension — keyword search on
chunks.textonly.
3.3 Embedding model
- Fix one model per deployment (e.g.
text-embedding-3-small, 1536-d). Document in agent.env.
4. Security notes
- Token on WS URL for v1 stub auth.
- direct channel: treat as user input; strip control chars; max length 8 KiB.
- Rate-limit
deck.lineperactorId(e.g. 30/sec).
5. Reference implementation layout
services/gateway/— Tish:npm run gateway(ortish run --features ws,process services/gateway/main.tish). Listens on ws://127.0.0.1:35987 (orCODJ_HUB_PORT).services/agent-worker/— Tish agent:npm run agent(ortish run --features ws,http,fs,process services/agent-worker/main.tish). It calls its LLM (callLLMover thehttpfeature) for real responses whenGRADIENT_MODEL_ACCESS_KEY(orMODEL_ACCESS_KEY) is set, and falls back to a demo patch otherwise. Memory persistence and RAG (§2.4, §3) remain planned — the worker is in-memory only.
See repository package.json / README for run commands.
6. Troubleshooting
Agent gets "HTTP error: 200 OK" when connecting
The WebSocket gateway responds with 101 Switching Protocols, not 200. A 200 OK response means another process is handling the port (e.g. an old Node server or another HTTP server).
- Start the gateway first:
npm run gateway— you should seeWebSocket server listening on ws://0.0.0.0:35987. - Check what is using the port: with the gateway running, run:
``bash
lsof -i :35987
`
You should see a single process (the tish gateway). If you see another process (e.g. node`), stop it so only the gateway is listening.
- Then start the agent:
npm run agent.