90 lines · 3.6 KB
import { agentClientId, encodeAwareness } from "./room-awareness";

const AGENT_PRESENCE_MS = 30 * 1000;
const AGENT_COLOR = "#a371f7";

/** Agents are keyed by (user, name) so two people's "claude" don't overwrite each other's presence. */
export function agentKey(userId: string, agent: string): string {
  return `${userId}:${agent}`;
}

export type TextEdit = { from: number; to: number; insert: string };
export type ApplyResult =
  | { ok: true; version: number; text: string }
  | { ok: false; error: "stale" | string; version: number; text: string };
type AgentRow = { name: string; user_name: string; path: string | null; clock: number };

/** Records an agent's latest activity and returns the awareness update announcing its cursor. */
export function touchAgent(sql: SqlStorage, agent: string, user: string, path: string, cursor: number): Uint8Array {
  const row = sql.exec<{ clock: number }>("SELECT clock FROM agents WHERE name = ?", agent).toArray()[0];
  const clock = (row?.clock ?? 0) + 1;
  sql.exec(
    `INSERT INTO agents (name, user_name, path, clock, expires_at) VALUES (?, ?, ?, ?, ?)
     ON CONFLICT(name) DO UPDATE SET user_name = excluded.user_name, path = excluded.path, clock = excluded.clock, expires_at = excluded.expires_at`,
    agent,
    user,
    path,
    clock,
    Date.now() + AGENT_PRESENCE_MS,
  );
  return agentUpdate({ name: agent, user_name: user, path, clock, cursor });
}

export function liveAgents(sql: SqlStorage): AgentRow[] {
  return sql
    .exec<AgentRow>("SELECT name, user_name, path, clock FROM agents WHERE expires_at > ?", Date.now())
    .toArray();
}

export function expireAgents(sql: SqlStorage): Array<{ name: string; clock: number }> {
  const expired = sql