Skip to content
54 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)1/**
2 * Which conversations an agent may read from while it answers in one
The docs folder is gone, and what it held lives where people read it: how a self-hosted g1t runs and how to deploy g1t to Cloudflare are pages on docs.g1t.sh under Run g1t yourself, and speed, rate limits and operating g1t.sh are sections of CONTRIBUTING.md; code that cited a file in docs/ now points to the page or section that covers it, or says what it means itself, and applied migrations and the runner images are left as they were.3 * (docs.g1t.sh/guides/agent-access/, "What an agent can and can't
4 * know"). Pure, so the rule is tested apart from the service,
5 * adversarially.
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)6 *
7 * The audience is everyone who will read the answer: a direct message's or
8 * private channel's people; for a public channel, the whole workspace. An
9 * agent may quote a conversation only when every one of them could read it
10 * themselves:
11 * - a public channel: always, since anyone in the workspace can open it;
12 * - a private channel: when every person in the audience is in it;
13 * - a direct message: only when it has exactly the audience's people, so
14 * an agent never carries one DM into another, even with the same person
15 * and someone more.
16 * A public channel's audience, or any of more than `SHARED_OVER` people,
17 * is the whole workspace: only public channels are readable to it.
18 */
19
20export type AudienceKind = "dm" | "private" | "public";
21
22/** Past this many people, an audience is the whole workspace's. */
23export const SHARED_OVER = 50;
24
25export type Conversation = { kind: "channel" | "dm"; private: boolean | number; user_ids: string[] };
26
27export function audienceKind(channel: { kind: "channel" | "dm"; private: boolean | number }): AudienceKind {
28 if (channel.kind === "dm") return "dm";
29 return channel.private ? "private" : "public";
30}
31
32/** Whether the audience is the workspace's as a whole. */
33export function isShared(audience: { kind: AudienceKind; user_ids: string[] }): boolean {
34 return audience.kind === "public" || audience.user_ids.length > SHARED_OVER;
35}
36
37/** Whether everyone in `audience` may read `target`. */
38export function readableBy(target: Conversation, audience: { kind: AudienceKind; user_ids: string[] }): boolean {
39 if (target.kind === "channel" && !target.private) return true;
40 if (isShared(audience) || !audience.user_ids.length) return false;
41 const members = new Set(target.user_ids);
42 if (target.kind === "dm") {
43 const people = new Set(audience.user_ids);
44 return members.size === people.size && [...people].every((id) => members.has(id));
45 }
46 return audience.user_ids.every((id) => members.has(id));
47}
48
49/** A search's words as a LIKE pattern, its wildcards escaped with `\`. */
50export function likePattern(query: string): string | null {
51 const words = String(query ?? "").trim().slice(0, 200);
52 if (words.length < 2) return null;
53 return `%${words.replace(/[\\%_]/g, (c) => `\\${c}`)}%`;
54}

This file's history is long; its oldest lines are credited to the oldest commit read.