Video calls (PairUX→ASCII), agent@ chat, and finger routes

ssh video-<code>@ joins a PairUX/LiveKit call rendered as truecolor
ASCII (▀ half-blocks, 2 pixels per cell); video@ prompts for a code.
Codes are minted by PairUX only. Pipeline: VP8 RTP → PLI keyframe
requests → ivfwriter remux → ffmpeg decode/scale → RGB24 → ANSI →
bubbletea over the SSH PTY. Subscriber-only, no audio in v1.

ssh agent@ opens a persisted chat with the operator's agent —
AGENTBBS_AGENT_CMD runs per message (stdin→stdout), e.g. `claude -p`.

ssh <member>@ with someone else's name prints a classic finger card
(.plan, member since, last seen) and disconnects; your own name still
enters the hub.

cmd/lkpublish: dev publisher for testing (explicit -fps pacing; lksdk
IVF replay mispaces from file timebase alone, measured 1fps from a
15fps file; dimensions required or dynacast pauses the track).

Verified end-to-end against livekit-server --dev: 128k truecolor cells
/ 20k distinct colors streamed over a real SSH session; chat round-trip
and finger card verified over SSH. Go toolchain pinned to 1.26 via
mise.toml (lksdk requirement).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Anthony Ettinger 2026-06-11 11:43:38 +00:00
parent f3b085a08f
commit 9b0f465946
14 changed files with 1397 additions and 46 deletions

View file

@ -35,6 +35,10 @@ type Store interface {
EnsureUser(name, kind, pubkeyFP string) (User, error)
// UserByFingerprint finds an account by SSH key fingerprint.
UserByFingerprint(fp string) (User, bool, error)
// UserByName finds an account by exact username (no creation).
UserByName(name string) (User, bool, error)
// LastSeen reports the start of the user's most recent session.
LastSeen(userID int64) (time.Time, bool, error)
RecordSession(userID int64, username, remote, route string) (int64, error)
EndSession(sessionID int64) error
@ -46,9 +50,20 @@ type Store interface {
PodPaidUntil(userID int64) (time.Time, bool, error)
GrantPod(userID int64, until time.Time, paymentRef string) error
// Chat transcripts for the agent@ surface.
AddChat(userID int64, username, role, text string) error
RecentChats(username string, n int) ([]ChatMessage, error)
Close() error
}
// ChatMessage is one line of an agent@ conversation.
type ChatMessage struct {
Role string // "user" or "agent"
Text string
At time.Time
}
// ErrKeyMismatch means a username is already registered with another key.
var ErrKeyMismatch = errors.New("username registered with a different key")
@ -92,6 +107,15 @@ CREATE TABLE IF NOT EXISTS scores (
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
);
CREATE INDEX IF NOT EXISTS idx_scores_game ON scores(game, score DESC);
CREATE TABLE IF NOT EXISTS chat_messages (
id INTEGER PRIMARY KEY,
user_id INTEGER,
username TEXT NOT NULL,
role TEXT NOT NULL,
text TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
);
CREATE INDEX IF NOT EXISTS idx_chat_user ON chat_messages(username, id);
CREATE TABLE IF NOT EXISTS pod_subscriptions (
user_id INTEGER PRIMARY KEY REFERENCES users(id),
paid_until TEXT NOT NULL,
@ -211,4 +235,65 @@ func (s *sqliteStore) GrantPod(userID int64, until time.Time, ref string) error
return err
}
func (s *sqliteStore) UserByName(name string) (User, bool, error) {
var u User
var created string
err := s.db.QueryRow(`SELECT id, name, kind, pubkey_fp, created_at FROM users WHERE name = ?`, name).
Scan(&u.ID, &u.Name, &u.Kind, &u.PubKeyFP, &created)
if err == sql.ErrNoRows {
return User{}, false, nil
}
if err != nil {
return User{}, false, err
}
u.CreatedAt, _ = time.Parse(time.RFC3339, created)
return u, true, nil
}
func (s *sqliteStore) LastSeen(userID int64) (time.Time, bool, error) {
var at string
err := s.db.QueryRow(`SELECT started_at FROM sessions WHERE user_id = ? ORDER BY id DESC LIMIT 1`, userID).Scan(&at)
if err == sql.ErrNoRows {
return time.Time{}, false, nil
}
if err != nil {
return time.Time{}, false, err
}
t, err := time.Parse(time.RFC3339, at)
return t, err == nil, err
}
func (s *sqliteStore) AddChat(userID int64, username, role, text string) error {
var uid any
if userID > 0 {
uid = userID
}
_, err := s.db.Exec(`INSERT INTO chat_messages (user_id, username, role, text) VALUES (?,?,?,?)`,
uid, username, role, text)
return err
}
func (s *sqliteStore) RecentChats(username string, n int) ([]ChatMessage, error) {
rows, err := s.db.Query(`
SELECT role, text, created_at FROM (
SELECT id, role, text, created_at FROM chat_messages
WHERE username = ? ORDER BY id DESC LIMIT ?
) ORDER BY id ASC`, username, n)
if err != nil {
return nil, err
}
defer rows.Close()
var out []ChatMessage
for rows.Next() {
var m ChatMessage
var at string
if err := rows.Scan(&m.Role, &m.Text, &at); err != nil {
return nil, err
}
m.At, _ = time.Parse(time.RFC3339, at)
out = append(out, m)
}
return out, rows.Err()
}
func (s *sqliteStore) Close() error { return s.db.Close() }