[[
wikihub
]]
Search
⌘K
Explore
People
For Agents
Sign in
Explore
People
For Agents
Sign in
@jacobcole / picortex — planning / docs/wiki/architecture.md
Suggest edit
Cancel
Submit suggestion
Title
Name
Note
--- visibility: public --- # Architecture ## One-paragraph summary A chat (1:1 or group) arrives via Linq webhook. picortex looks up or provisions a Unix user + home dir + tmux session for that chat's stable ID. The inbound message is appended to the canonical SQLite log, then — after attention gating — injected into the chat's tmux session via `send-keys`, wrapped in turn sentinels. Claude Code's response is parsed out of the tmux pipe-pane log and sent back via Linq. A mobile-first web UI attaches to the same tmux session through an xterm.js WebSocket bridge and displays a browseable file tree of the chat's home. ## Component diagram ``` ┌────────────────────────────────────────────────────────────────────────┐ │ Linq (or linq-sim) │ └────────────┬───────────────────────────────────────┬───────────────────┘ │ HMAC-signed webhook │ /api/partner/v3/* ▼ ▲ ┌───────────────────────────────────────────────────┴────┐ │ picortex backend (Fastify, TS, pino) │ │ │ │ Channel<Linq> ─▶ Router ─▶ AttentionGate ─▶ │ │ │ │ │ │ ▼ ▼ │ │ SQLite log TurnDispatcher │ │ │ │ │ │ ▼ │ │ │ runuser -u chat-X │ │ │ │ │ │ ▼ ▼ │ │ ┌─────────────────────────────────┐ │ │ │ tmux picortex:<chat_id> │ │ │ │ └─ claude (Claude Code) │ │ │ │ └─ pipe-pane → session.log │ │ │ └─────────────────────────────────┘ │ │ ▲ ▲ │ │ │ │ │ │ ReplyCapture ────────┘ │ │ │ │ │ │ │ ▼ │ │ │ Channel<Linq>.send │ │ │ │ │ │ /ws/terminal/:chat_id ───────────────┘ node-pty │ │ │ └─────────────────────────────────────────────────────────┘ ▲ ▲ │ HTTP + WS │ HTTP (files API) │ │ ┌───────────────────────────────────────────────────┐ ┌──────┐ │ Frontend (Vite + React, :7824) │ │ ... │ │ swipe-panels: messages │ files │ terminal │ └──────┘ └───────────────────────────────────────────────────┘ ``` ## Invariants - **I1 Canonical log:** the SQLite `messages` table is authoritative. The workspace FS is a cache. - **I2 Per-chat user:** no chat's bytes ever reach another chat's process, enforced by POSIX perms. - **I3 Sentinel protocol:** every Claude turn is bracketed in tmux by unique start/end sentinels so reply capture is unambiguous. - **I4 Backend-as-root:** the backend can enter any chat user via sudoers; chat users have no sudo. - **I5 Channel interface:** business logic does not know about Linq, only about the `Channel` interface. - **I6 HMAC on inbound:** no unsigned webhook is processed. Full stop. ## Data model (SQLite) ```sql CREATE TABLE chats ( id TEXT PRIMARY KEY, -- from Linq kind TEXT NOT NULL, -- '1on1' | 'group' display_name TEXT, owner_phone TEXT NOT NULL, unix_user TEXT UNIQUE NOT NULL, home_dir TEXT NOT NULL, created_at INTEGER NOT NULL, last_message_at INTEGER ); CREATE TABLE messages ( id TEXT PRIMARY KEY, chat_id TEXT NOT NULL REFERENCES chats(id), direction TEXT NOT NULL, -- 'inbound' | 'outbound' author TEXT, -- phone or bot text TEXT, reply_to_message_id TEXT, turn_id TEXT, -- present on outbound, ties to tmux turn request_id TEXT NOT NULL, raw_event JSON, created_at INTEGER NOT NULL ); CREATE TABLE events ( id INTEGER PRIMARY KEY AUTOINCREMENT, kind TEXT NOT NULL, chat_id TEXT, request_id TEXT, payload JSON, created_at INTEGER NOT NULL ); CREATE TABLE bridge_events ( id INTEGER PRIMARY KEY AUTOINCREMENT, src_chat_id TEXT NOT NULL, dst_chat_id TEXT NOT NULL, path TEXT NOT NULL, sha256 TEXT NOT NULL, approver_phone TEXT NOT NULL, challenge_message_id TEXT NOT NULL, approval_message_id TEXT NOT NULL, created_at INTEGER NOT NULL ); CREATE TABLE chat_config ( chat_id TEXT PRIMARY KEY REFERENCES chats(id), attention_mode TEXT NOT NULL DEFAULT 'mentions-only', discriminator_model TEXT, discriminator_threshold REAL, updated_at INTEGER NOT NULL ); ``` ## Runtime - **picortex service:** systemd unit `picortex.service`, starts Fastify + Vite-built static frontend. - **tmux per chat:** spawned on demand under the chat user. - **cron:** `picortex-lifecycle.timer` runs hourly for idle reap + archive.