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.
| Inbox: the events service tells people what needs them as events arrive | 1 | /** |
| 2 | * The pure parts of request timing and D1 read consistency for the site: | |
| 3 | * the `g1t_d1` cookie, which session each service call asks for, which | |
| 4 | * calls may write, and the `Server-Timing` header. perf.server.ts holds | |
| 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. | 5 | * the per-request state; CONTRIBUTING.md, "Speed", says how to use it. |
| Inbox: the events service tells people what needs them as events arrive | 6 | */ |
| 7 | ||
| 8 | /** The cookie that carries D1 bookmarks between a person's requests. */ | |
| 9 | export const D1_COOKIE = "g1t_d1"; | |
| 10 | ||
| 11 | /** | |
| 12 | * How long after a write the services a request did not get a bookmark | |
| 13 | * from read their primary. A write can reach a service the site did not | |
| 14 | * call itself (work writing to repos, say), whose bookmark the site never | |
| 15 | * sees; D1 replicas trail the primary by well under a second, so 30 | |
| 16 | * seconds covers that with room to spare. | |
| 17 | */ | |
| 18 | export const PRIMARY_WINDOW_SECONDS = 30; | |
| 19 | ||
| 20 | /** How long the bookmarks are kept: far longer than any replica trails. */ | |
| 21 | export const D1_COOKIE_MAX_AGE = 300; | |
| 22 | ||
| 23 | /** | |
| 24 | * The services whose RPCs open a D1 session (crates/kit/src/d1.rs and | |
| 25 | * @g1t/contracts d1.ts), by the name the site gives their binding. | |
| 26 | */ | |
| 27 | export const SESSION_SERVICES = new Set(["identity", "repos", "work", "search", "billing", "projects", "deployments"]); | |
| 28 | ||
| 29 | /** What the site remembers from a person's last writes. */ | |
| 30 | export type Bookmarks = { | |
| 31 | /** Unix seconds of the last request that may have written, if recent. */ | |
| 32 | at: number | null; | |
| 33 | /** The latest bookmark each service returned after it. */ | |
| 34 | services: Record<string, string>; | |
| 35 | }; | |
| 36 | ||
| 37 | const BOOKMARK = /^[0-9A-Za-z-]{1,256}$/; | |
| 38 | const SERVICE = /^[a-z]{1,32}$/; | |
| 39 | ||
| 40 | /** Reads the `g1t_d1` cookie; anything malformed is dropped. */ | |
| 41 | export function readBookmarks(cookieHeader: string | null): Bookmarks { | |
| 42 | const empty: Bookmarks = { at: null, services: {} }; | |
| 43 | if (!cookieHeader) return empty; | |
| 44 | const match = new RegExp(`(?:^|;\\s*)${D1_COOKIE}=([^;]*)`).exec(cookieHeader); | |
| 45 | if (!match) return empty; | |
| 46 | const found: Bookmarks = { at: null, services: {} }; | |
| 47 | for (const entry of match[1].split("~")) { | |
| 48 | const colon = entry.indexOf(":"); | |
| 49 | if (colon < 1) continue; | |
| 50 | const key = entry.slice(0, colon); | |
| 51 | const value = entry.slice(colon + 1); | |
| 52 | if (key === "at") { | |
| 53 | const at = Number(value); | |
| 54 | if (Number.isSafeInteger(at) && at > 0) found.at = at; | |
| 55 | } else if (SERVICE.test(key) && SESSION_SERVICES.has(key) && BOOKMARK.test(value)) { | |
| 56 | found.services[key] = value; | |
| 57 | } | |
| 58 | } | |
| 59 | return found; | |
| 60 | } | |
| 61 | ||
| 62 | /** The `g1t_d1` cookie's value. */ | |
| 63 | export function writeBookmarks(bookmarks: Bookmarks): string { | |
| 64 | const entries = bookmarks.at ? [`at:${bookmarks.at}`] : []; | |
| 65 | for (const [service, bookmark] of Object.entries(bookmarks.services).sort()) { | |
| 66 | if (SESSION_SERVICES.has(service) && BOOKMARK.test(bookmark)) entries.push(`${service}:${bookmark}`); | |
| 67 | } | |
| 68 | return entries.join("~"); | |
| 69 | } | |
| 70 | ||
| 71 | /** The `Set-Cookie` value for `bookmarks`. */ | |
| 72 | export function bookmarkCookie(bookmarks: Bookmarks, secure: boolean): string { | |
| 73 | return `${D1_COOKIE}=${writeBookmarks(bookmarks)}; Path=/; HttpOnly;${secure ? " Secure;" : ""} SameSite=Lax; Max-Age=${D1_COOKIE_MAX_AGE}`; | |
| 74 | } | |
| 75 | ||
| 76 | /** | |
| 77 | * What a call to `service` sends as `x-d1-bookmark`, or null for none | |
| 78 | * (that service reads its primary, as before sessions): | |
| 79 | * | |
| 80 | * - A request that writes (any method but GET and HEAD) starts every | |
| 81 | * session on the primary, so what it checks before writing is current. | |
| 82 | * - Within `PRIMARY_WINDOW_SECONDS` of the person's last write, the | |
| 83 | * primary, for every service: that write may have reached a service | |
| 84 | * through another one, whose bookmark the site never saw. | |
| 85 | * - Otherwise the bookmark that service returned after that write: never | |
| 86 | * older than what they did, however far a replica trails. | |
| 87 | * - Otherwise the nearest copy. | |
| 88 | */ | |
| 89 | export function sessionFor(service: string, bookmarks: Bookmarks, writing: boolean, nowSeconds: number): string | null { | |
| 90 | if (!SESSION_SERVICES.has(service)) return null; | |
| 91 | if (writing) return "first-primary"; | |
| 92 | if (bookmarks.at && nowSeconds - bookmarks.at < PRIMARY_WINDOW_SECONDS) return "first-primary"; | |
| 93 | return bookmarks.services[service] ?? "first-unconstrained"; | |
| 94 | } | |
| 95 | ||
| 96 | /** | |
| 97 | * Methods that only read. Anything not listed is taken to write, so a new | |
| 98 | * method errs towards a cookie and a primary read, never a stale page. | |
| 99 | * `get`, `list`, `queue` and `pulls_for_repos` were missing: every project | |
| 100 | * page called `get` (repos, projects), so every one set the cookie, was | |
| 101 | * never kept in the public cache, and sent the next 30 s of the person's | |
| Public pages cacheable again: a signed-out GET never sets g1t_d1; stars, about and public_links are reads | 102 | * reads to the primary. `stars`, `about` and `public_links` (2026-10-08) |
| Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 25 | 103 | * did the same to every project page and Explore for 13 hours. `tags`, |
| 104 | * `commit_checks` and `shortcuts` (every overview), and the Files page's | |
| 105 | * `last_commits`, `languages`, `contributors` and `license`, still made a | |
| 106 | * signed-in view count as a write until 2026-10-08: the cookie, 30 s of | |
| 107 | * primary reads, and no sidebar cache after every overview. | |
| Inbox: the events service tells people what needs them as events arrive | 108 | */ |
| 109 | const READS = new Set( | |
| 110 | ( | |
| Chat controls, public profiles, shadcn selects, and no Docs tab in a project | 111 | "account active_agents all_ids get list queue pulls_for_repos blame blob branches by_author by_repo contributions catalog check_invite check_limit " + |
| Inbox: the events service tells people what needs them as events arrive | 112 | "check_workspace_deletion check_workspace_rename collaborator_permission compare counts deleted deliveries " + |
| Projects no longer depend on each other: the dependsOn relation, its settings page (old links redirect), the overview card, a pull request's Affects panel and preview stacks, reference variables and agents' notes on what a project uses are gone; the table is dropped in a later deploy | 113 | "domains entitlements entity explore features git_access has_feature invoices ledger limit " + |
| Memory is gone: people's own harnesses keep their own memory (CLAUDE.md, AGENTS.md, auto-memory), so g1t's project and workspace notes that agents recalled and remembered only duplicated them. The recall and remember operations, their /repos/{owner}/{name}/memory routes, the memory MCP tool and its lines in the MCP instructions, the Memory section of the API reference and OpenAPI document (and the CLI's table generated from it) are removed, as are the work service's memory methods, the Memory pages of a project and of a workspace with their sidebar links, Home's Decisions card (which read decisions out of workspace memory), the Memory guide (its address and the old agent-memory, agents-and-memory and context-hub ones now lead to Connect a coding agent) and every mention in the docs, README, llms.txt, the landing page, the privacy policy and the Ask bench. Decision records, g1t why and g1t work stay as they were. memory:read and memory:write are retired as agents:run and the models scopes were: tokens and OAuth grants that hold them still parse and keep working for their other scopes, but no preset (the Agent preset and the OAuth default included), token form, mobile sign-in or hosted session hands them out and nothing needs them; looking up a ticket (search ticket, get_context), which needed memory:read, needs issues:read. The memories table stays in work's D1 untouched and is no longer read or written, except that purging a repository or deleting a workspace still deletes its old rows. Checked: cargo test for g1t-contracts (308), g1t-api (201), g1t (102 and 41), g1t-work (103) and g1t-integrations (65), cargo check of the workspace, the OpenAPI document and ops table regenerated, every workspace's typecheck, the site's 901 tests and a production build, the contracts' 48 tests, the phone app's 11, the deploy tool's 92, and the docs' tests and build. | 114 | "limit_requests links log logs managed_pulls my_repo_invitations " + |
| Inbox: the events service tells people what needs them as events arrive | 115 | "outside_collaborators overview path_by_id prices profile profile_workspaces public_namespaces read_session " + |
| 116 | "readable ready_issues references registration repo_access resolve resolve_branch resolve_path resolve_slug " + | |
| Memory is gone: people's own harnesses keep their own memory (CLAUDE.md, AGENTS.md, auto-memory), so g1t's project and workspace notes that agents recalled and remembered only duplicated them. The recall and remember operations, their /repos/{owner}/{name}/memory routes, the memory MCP tool and its lines in the MCP instructions, the Memory section of the API reference and OpenAPI document (and the CLI's table generated from it) are removed, as are the work service's memory methods, the Memory pages of a project and of a workspace with their sidebar links, Home's Decisions card (which read decisions out of workspace memory), the Memory guide (its address and the old agent-memory, agents-and-memory and context-hub ones now lead to Connect a coding agent) and every mention in the docs, README, llms.txt, the landing page, the privacy policy and the Ask bench. Decision records, g1t why and g1t work stay as they were. memory:read and memory:write are retired as agents:run and the models scopes were: tokens and OAuth grants that hold them still parse and keep working for their other scopes, but no preset (the Agent preset and the OAuth default included), token form, mobile sign-in or hosted session hands them out and nothing needs them; looking up a ticket (search ticket, get_context), which needed memory:read, needs issues:read. The memories table stays in work's D1 untouched and is no longer read or written, except that purging a repository or deleting a workspace still deletes its old rows. Checked: cargo test for g1t-contracts (308), g1t-api (201), g1t (102 and 41), g1t-work (103) and g1t-integrations (65), cargo check of the workspace, the OpenAPI document and ops table regenerated, every workspace's typecheck, the site's 901 tests and a production build, the contracts' 48 tests, the phone app's 11, the deploy tool's 92, and the docs' tests and build. | 117 | "routes run run_context run_cost runner_activity runner_groups runner_settings runners runs scorecards search usage_report " + |
| Inbox: the events service tells people what needs them as events arrive | 118 | "settings statement statement_entries status status_by_id suggest tree usage usage_meters user_by_username " + |
| Merge main into Artifacts Phase 2 | 119 | "user_for_session user_for_access_token usernames waiting_workspaces workflows workspace workspace_invites github_enabled " + |
| Merge project overview: one branch_drift call, spliced histories, cached tags, 6 repos calls instead of 25 | 120 | "stars about public_links branch_drift tags last_commits languages contributors license releases release " + |
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9 | 121 | "stargazers starred commit_checks shortcuts spend person_budgets usage_report templates install_requests " + |
| 122 | "marketplaces discover plugin plugin_installs plugin_agents connectors connector agent_connectors customize_bundle " + | |
| Home says what people did as well as what agents did. Since you were last here now has a People column and an Agents column: who pushed how many commits to which projects, pull requests opened, merged and reviewed, issues opened and closed, docs edited, messages sent and deploys that went out, each line a link to where those are listed, with the agents' acceptance (first time, after review, didn't finish) kept as a row of their column; the sentence under the heading sums it up honestly, and says when a part of g1t could not be read, or that the span was quiet. Landed counts merged pull requests, commits pushed straight to a default branch, production deploys that went live, releases and packages, newest first; Running now adds workflow runs. Behind it, the events service answers an activity digest over a span in one round trip from its existing indexes, every push now records how many commits it carried, and chat counts the messages sent in the conversations you can read. The Home guide defines every line, and says how this scales. | 123 | "people_directory team_agents team_context team_members github_visible_installations skill_library skill agent_skills last_visit team_agent_index " + |
| 124 | "activity_digest activity" | |
| Inbox: the events service tells people what needs them as events arrive | 125 | ).split(" "), |
| 126 | ); | |
| 127 | ||
| Public pages cacheable again: a signed-out GET never sets g1t_d1; stars, about and public_links are reads | 128 | /** |
| 129 | * Whether a response sets the `g1t_d1` cookie. A signed-out GET never | |
| 130 | * does, whatever it called: nobody signed out can write anything their | |
| 131 | * next page must read, and a cookie keeps the page out of the public | |
| 132 | * cache. Signing in on a GET (GitHub) starts a session, so it does. | |
| 133 | */ | |
| 134 | export function setsBookmark(request: { writing: boolean; wrote: boolean; hasSession: boolean; signedIn: boolean }): boolean { | |
| 135 | if (request.writing || request.signedIn) return true; | |
| 136 | return request.wrote && request.hasSession; | |
| 137 | } | |
| 138 | ||
| Inbox: the events service tells people what needs them as events arrive | 139 | /** Whether an RPC to `method` may write. */ |
| 140 | export function mayWrite(method: string): boolean { | |
| 141 | if (READS.has(method)) return false; | |
| 142 | return !(method.startsWith("get_") || method.startsWith("list_")); | |
| 143 | } | |
| 144 | ||
| 145 | /** The method name of an RPC URL (`https://service/rpc/<method>`). */ | |
| 146 | export function rpcMethodOf(input: string): string { | |
| 147 | const at = input.indexOf("/rpc/"); | |
| 148 | return at < 0 ? "" : input.slice(at + 5).split(/[?#]/)[0]; | |
| 149 | } | |
| 150 | ||
| 151 | /** One service's calls during a request. */ | |
| 152 | export type ServiceTiming = { | |
| 153 | calls: number; | |
| 154 | /** Summed wall time of its calls, from here. */ | |
| 155 | wallMs: number; | |
| 156 | /** Summed time its own `server-timing: svc;dur` reported. */ | |
| 157 | serviceMs: number; | |
| 158 | /** | |
| 159 | * Of that, summed time it reported waiting on its database | |
| 160 | * (`db;dur`, crates/kit/src/d1.rs `Timing`) and in how many round trips. | |
| 161 | * Absent for services that do not time them. | |
| 162 | */ | |
| 163 | dbMs?: number; | |
| 164 | dbTrips?: number; | |
| 165 | }; | |
| 166 | ||
| 167 | /** The `svc;dur=N` a service reports, or null. */ | |
| 168 | export function serviceDuration(header: string | null): number | null { | |
| 169 | if (!header) return null; | |
| 170 | const match = /(?:^|,)\s*svc;dur=([0-9.]+)/.exec(header); | |
| 171 | return match ? Number(match[1]) : null; | |
| 172 | } | |
| 173 | ||
| 174 | /** | |
| 175 | * The `db;dur=N;desc="T round trips, …"` a service reports: its summed | |
| 176 | * database time and round trips, or null. | |
| 177 | */ | |
| 178 | export function databaseTime(header: string | null): { ms: number; trips: number } | null { | |
| 179 | if (!header) return null; | |
| 180 | const match = /(?:^|,)\s*db;dur=([0-9.]+)(?:;desc="(\d+) round trips?)?/.exec(header); | |
| 181 | return match ? { ms: Number(match[1]), trips: Number(match[2] ?? 0) } : null; | |
| 182 | } | |
| 183 | ||
| 184 | /** Total time covered by overlapping [start, end] intervals. */ | |
| 185 | export function coveredMs(intervals: [number, number][]): number { | |
| 186 | const sorted = [...intervals].sort((a, b) => a[0] - b[0]); | |
| 187 | let total = 0; | |
| 188 | let end = -Infinity; | |
| 189 | let start = -Infinity; | |
| 190 | for (const [from, to] of sorted) { | |
| 191 | if (from > end) { | |
| 192 | if (end > start) total += end - start; | |
| 193 | start = from; | |
| 194 | end = to; | |
| 195 | } else if (to > end) { | |
| 196 | end = to; | |
| 197 | } | |
| 198 | } | |
| 199 | if (end > start) total += end - start; | |
| 200 | return total; | |
| 201 | } | |
| 202 | ||
| 203 | /** A Server-Timing metric name: a token, so `/` and spaces become `.`. */ | |
| 204 | export function metricName(name: string): string { | |
| 205 | return name.replace(/^routes\//, "").replace(/[^A-Za-z0-9_.-]+/g, "."); | |
| 206 | } | |
| 207 | ||
| 208 | /** | |
| 209 | * The `Server-Timing` header for a request: the whole, the loaders, the | |
| 210 | * time spent waiting on services (overlap counted once), then each | |
| 211 | * service. DevTools shows them in this order under Network → Timing. | |
| 212 | */ | |
| 213 | export function serverTiming(input: { | |
| 214 | totalMs: number; | |
| 215 | loaders: { id: string; ms: number; kind: "loader" | "action" }[]; | |
| 216 | rpcMs: number; | |
| 217 | services: Record<string, ServiceTiming>; | |
| 218 | sessions: string; | |
| 219 | }): string { | |
| 220 | const parts = [`total;dur=${input.totalMs};desc="web to first byte"`]; | |
| 221 | for (const loader of input.loaders) { | |
| 222 | parts.push(`${loader.kind}.${metricName(loader.id)};dur=${loader.ms}`); | |
| 223 | } | |
| 224 | const calls = Object.values(input.services).reduce((sum, timing) => sum + timing.calls, 0); | |
| 225 | if (calls > 0) parts.push(`rpc;dur=${input.rpcMs};desc="${calls} service calls, overlap counted once"`); | |
| 226 | const ranked = Object.entries(input.services).sort((a, b) => b[1].wallMs - a[1].wallMs); | |
| 227 | for (const [name, timing] of ranked) { | |
| 228 | const db = timing.dbTrips ? `, db ${timing.dbMs ?? 0}ms in ${timing.dbTrips} round trip${timing.dbTrips === 1 ? "" : "s"}` : ""; | |
| 229 | const inside = timing.serviceMs > 0 ? `, ${timing.serviceMs}ms inside${db}` : ""; | |
| 230 | parts.push(`${metricName(name)};dur=${timing.wallMs};desc="${timing.calls} call${timing.calls === 1 ? "" : "s"}${inside}"`); | |
| 231 | } | |
| 232 | if (input.sessions) parts.push(`d1;desc="${input.sessions}"`); | |
| 233 | return parts.join(", "); | |
| 234 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.