Skip to content
150 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.

Docs: a workspace knowledge base people and agents write together1/**
2 * What a page's room saves to D1 after a burst of edits: the Markdown
3 * rendition, the search index, backlinks, history, and telling people
4 * newly mentioned in the page. Run by the room (src/room.ts), which owns
5 * the live document; nothing here reads the document itself.
6 */
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store7import { newId, notifyClient, type DocCitation, type DocVersionKind, type FeedNotification, type ServiceBinding } from "@g1t/contracts";
Docs: a workspace knowledge base people and agents write together8
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store9import { publishDocEvent } from "./events.ts";
Docs: a workspace knowledge base people and agents write together10import { excerpt, searchText } from "./markdown.ts";
11import { linkedPageIds, pageSlug } from "./slugs.ts";
12
13/** How long edits gather into one version before the next starts. */
14export const VERSION_EVERY_MS = 10 * 60 * 1000;
15/** The largest Yjs state a version keeps; past it, only its Markdown. */
16const MAX_VERSION_STATE = 1_500_000;
17
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store18export type SaveEnv = { DB: D1Database; NOTIFY?: ServiceBinding; EVENTS?: ServiceBinding };
Docs: a workspace knowledge base people and agents write together19
20export type Save = {
21 page_id: string;
22 markdown: string;
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store23 /** Code the document cites (src/citations.ts `bodyCitations`). */
24 citations: DocCitation[];
Docs: a workspace knowledge base people and agents write together25 /** The editors since the last save, member keys, last one last. */
26 editors: string[];
27 /** People mentioned in the document now (usernames, lowercased). */
28 mentioned: string[];
29 /** The usernames of the people who made these edits, lowercased: nobody is told they mentioned themselves. */
30 editor_names: string[];
31 /** The whole document, for a version. */
32 state: Uint8Array;
33 /** A version to record now, whatever the time since the last. */
34 version: { kind: DocVersionKind; note: string | null; authors: string[] } | null;
35 /** Editors since the last version, for a timed one. */
36 pending_authors: string[];
37 /** When the last version was recorded (ms), or 0. */
38 last_version_at: number;
39 /** The workspace's slug as the room last heard it, for links in notifications. */
40 workspace_slug: string | null;
41};
42
43type PageRow = { id: string; workspace_id: string; space_id: string; title: string; mentioned: string; markdown: string; slug: string; archived_at: string | null };
44
45/** Saves; returns whether a version was recorded, and its id. */
46export async function save(env: SaveEnv, input: Save, now = new Date()): Promise<{ version_id: string | null }> {
47 const at = now.toISOString();
48 const page = await env.DB.prepare(
49 "SELECT p.id, p.workspace_id, p.space_id, p.title, p.mentioned, p.markdown, p.archived_at, s.slug AS slug FROM pages p JOIN spaces s ON s.id = p.space_id WHERE p.id = ?",
50 )
51 .bind(input.page_id)
52 .first<PageRow>();
53 if (!page) return { version_id: null };
54 const changed = page.markdown !== input.markdown;
55 const last = input.editors[input.editors.length - 1] ?? null;
56 const statements: D1PreparedStatement[] = [];
57 if (changed || last) {
58 statements.push(
59 env.DB.prepare("UPDATE pages SET markdown = ?, updated_at = ?, updated_by = COALESCE(?, updated_by) WHERE id = ?").bind(input.markdown, at, last, page.id),
60 );
61 }
62 if (changed) {
63 statements.push(env.DB.prepare("DELETE FROM pages_fts WHERE page_id = ?").bind(page.id));
64 statements.push(env.DB.prepare("INSERT INTO pages_fts (page_id, title, body) VALUES (?, ?, ?)").bind(page.id, page.title, searchText(input.markdown)));
65 statements.push(env.DB.prepare("DELETE FROM page_links WHERE from_page = ?").bind(page.id));
66 for (const to of linkedPageIds(input.markdown).filter((id) => id !== page.id).slice(0, 200)) {
67 statements.push(env.DB.prepare("INSERT OR IGNORE INTO page_links (from_page, to_page) VALUES (?, ?)").bind(page.id, to));
68 }
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store69 // What it cites, from its text; the header's own stay.
70 statements.push(env.DB.prepare("DELETE FROM citations WHERE page_id = ? AND source = 'body'").bind(page.id));
71 for (const c of input.citations) {
72 statements.push(
73 env.DB.prepare("INSERT OR IGNORE INTO citations (page_id, repo, path, kind, label, ref, source) VALUES (?, ?, ?, ?, ?, ?, 'body')").bind(page.id, c.repo, c.path, c.kind, c.label ?? "", c.ref),
74 );
75 }
Docs: a workspace knowledge base people and agents write together76 }
77 // A version: asked for (an agent's edit, a suggestion, a restore), or
78 // the first save after enough time since the last one.
79 let versionId: string | null = null;
80 const timed = input.pending_authors.length > 0 && now.getTime() - input.last_version_at >= VERSION_EVERY_MS;
81 if (input.version || (timed && changed)) {
82 versionId = newId("ver", now.getTime());
83 const authors = input.version?.authors.length ? input.version.authors : input.pending_authors;
84 const state = input.state.byteLength <= MAX_VERSION_STATE ? input.state : null;
85 statements.push(
86 env.DB.prepare("INSERT INTO page_versions (id, page_id, created_at, kind, authors, note, markdown, state) VALUES (?, ?, ?, ?, ?, ?, ?, ?)").bind(
87 versionId,
88 page.id,
89 at,
90 input.version?.kind ?? "edit",
91 JSON.stringify([...new Set(authors)]),
92 input.version?.note ?? null,
93 input.markdown,
94 state,
95 ),
96 );
97 }
98 // People mentioned for the first time.
99 let told: string[] = [];
100 try {
101 told = JSON.parse(page.mentioned) as string[];
102 } catch {
103 told = [];
104 }
105 const editors = new Set(input.editor_names);
106 const fresh = input.mentioned.filter((name) => !told.includes(name) && !editors.has(name));
107 if (fresh.length || input.mentioned.length !== told.length) {
108 statements.push(env.DB.prepare("UPDATE pages SET mentioned = ? WHERE id = ?").bind(JSON.stringify([...new Set([...told.filter((id) => input.mentioned.includes(id)), ...fresh])]), page.id));
109 }
110 if (statements.length) await env.DB.batch(statements);
Docs know what code they describe; a project's docs folder in Docs; Docs events; files on any S3 store111 // A version is what the rest of g1t hears of: at most every ten minutes of editing, and each agent edit, suggestion and restore.
112 const kind = input.version?.kind ?? "edit";
113 if (versionId && kind !== "created" && input.workspace_slug && !page.archived_at) {
114 await publishDocEvent(
115 env.EVENTS,
116 "doc.page.updated",
117 {
118 workspace: input.workspace_slug,
119 workspaceId: page.workspace_id,
120 pageId: page.id,
121 spaceId: page.space_id,
122 title: page.title,
123 path: `/${input.workspace_slug}/-/docs/${page.slug}/${pageSlug(page.title, page.id)}`,
124 versionId,
125 kind,
126 authors: [...new Set(input.version?.authors.length ? input.version.authors : input.pending_authors)],
127 },
128 last,
129 );
130 }
Docs: a workspace knowledge base people and agents write together131 if (fresh.length && env.NOTIFY && !page.archived_at) {
132 const slug = input.workspace_slug;
133 if (slug) {
134 const href = `/${slug}/-/docs/${page.slug}/${pageSlug(page.title, page.id)}`;
135 const notification = (username: string): FeedNotification => ({
136 id: `doc-mention:${page.id}:${username}`,
137 kind: "mention",
138 workspace: slug,
139 title: `You were mentioned in ${page.title || "Untitled"}`,
140 body: excerpt(input.markdown, 140),
141 href,
142 actor: { kind: last?.startsWith("agent:") ? "agent" : "user", id: last?.slice(last.indexOf(":") + 1) ?? "", name: "Docs" },
143 created_at: at,
144 });
145 const notify = notifyClient(env.NOTIFY);
146 await Promise.all(fresh.map((name) => notify.notify({ username: name }, notification(name)).catch(() => undefined)));
147 }
148 }
149 return { version_id: versionId };
150}