Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
629 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.

The work, events, webhooks, actions, repos and packages services no longer run g1t's own agents. Work drops the 35 calls that queued issues for g1t, delegated them, messaged and reviewed through the agent, planned, captured memories automatically, rated confidence and capped agent spend, and its pull request lifecycle no longer revises or reviews: a pull request only advances to a merge by the repository's auto-merge setting. Merging a pull request that is behind now catches it up itself, through the repository's branch update, and lands it when the push arrives; when it can't be caught up cleanly the merge says to merge the base in and try again. Recording a session on a pull request, memory recall and remember, overlaps, the merge queue and required checks stay as they were.1//! Runs: every sandbox g1t starts for checks, the merge queue and finding
2//! out whether a pull request merges cleanly, as a record people can watch
3//! and look back on. Runs of g1t's former agents are kept and listed too.
Inbox: the events service tells people what needs them as events arrive4//!
5//! The runner service opens a run as it starts a sandbox (`open_run`) and
6//! the sandbox reports its steps through the API with the run's one-time
7//! token (`report_run`), as checks do. The runner closes it when the
The work, events, webhooks, actions, repos and packages services no longer run g1t's own agents. Work drops the 35 calls that queued issues for g1t, delegated them, messaged and reviewed through the agent, planned, captured memories automatically, rated confidence and capped agent spend, and its pull request lifecycle no longer revises or reviews: a pull request only advances to a merge by the repository's auto-merge setting. Merging a pull request that is behind now catches it up itself, through the repository's branch update, and lands it when the push arrives; when it can't be caught up cleanly the merge says to merge the base in and try again. Recording a session on a pull request, memory recall and remember, overlaps, the merge queue and required checks stay as they were.8//! sandbox stops.
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.9//!
10//! Sessions are the read side of what coding agents record on a pull
11//! request (`append_session`): `list_sessions` and `get_session`.
Inbox: the events service tells people what needs them as events arrive12
13use g1t_contracts::agents::*;
14use g1t_contracts::repos::{Repo, RepoPath};
15use g1t_contracts::time::rfc3339;
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.16use g1t_contracts::work::{PullStatus, SessionEntry};
Inbox: the events service tells people what needs them as events arrive17use g1t_contracts::{FailureCode, Membership, Outcome, User, Viewer, new_id};
18use g1t_kit::now_ms;
19use serde::Deserialize;
20use worker::Result;
21use worker::wasm_bindgen::JsValue;
22
23use crate::checks::{hash, new_token};
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.24use crate::rows::SessionRow;
Inbox: the events service tells people what needs them as events arrive25use crate::{Work, optional};
26
Merge branch 'run-notes' into fast-push27/// How the runner starts a step that notes something about the sandbox
28/// rather than what the run is doing: a host it was refused (the runner's
29/// egress.ts `blockedStep`).
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.30/// A workspace renamed: its runs move to the slug it has now. `?1` is the
31/// current slug, `?2` a stale one (see `g1t_kit::rename`).
32pub(crate) const RENAMED: &[&str] = &[
33 "UPDATE agent_runs SET workspace = ?1 WHERE workspace = ?2",
34 "UPDATE agent_runs SET repo = ?1 || substr(repo, length(?2) + 1) WHERE substr(repo, 1, length(?2) + 1) = ?2 || '/'",
35];
36
37/// A repository transferred (see `g1t_kit::transfer`): its runs go with
38/// it, as its issues and pull requests do by id. `?1`/`?2` the path now
39/// and before, `?3`/`?4` the workspaces, `?5` the repository's id.
40pub(crate) const TRANSFERRED: &[&str] = &[
41 "UPDATE agent_runs SET workspace = ?3, repo = ?1 WHERE repo_id = ?5 AND repo = ?2",
42];
43
Merge branch 'run-notes' into fast-push44const NOTE_PREFIXES: [&str; 1] = ["Blocked: "];
45
46/// Where a report leaves the run: its newest step that says what the run
47/// is doing. A note (a refused host) stays in the steps but never stands
48/// for the run, which carries on past it. `None` when there is only notes.
49fn current_step(steps: &[String]) -> Option<String> {
50 steps.iter().rev().find(|step| !NOTE_PREFIXES.iter().any(|prefix| step.starts_with(prefix))).cloned()
51}
52
Inbox: the events service tells people what needs them as events arrive53/// The most steps a run keeps; older ones fall off the start.
54const MAX_STEPS: u32 = 200;
55/// The most steps taken from one report.
56const MAX_STEPS_PER_REPORT: usize = 20;
57/// The longest a step is kept.
58const MAX_STEP_CHARS: usize = 240;
59/// A run that has said nothing for this long is taken to have died.
60const SILENT_HOURS: u64 = 3;
61const DEFAULT_LIST: u32 = 50;
62const MAX_LIST: u32 = 200;
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.63/// The most session entries one view returns.
64const SESSION_ENTRIES: u32 = 2000;
Inbox: the events service tells people what needs them as events arrive65
66/// Every column but the steps, which only a run's own page needs.
67const COLUMNS: &str = "id, workspace, repo_id, repo, number, pull_id, kind, agent, model, status,
68 step, step_count, cost_usd, turns, sandbox, token_hash, started_by, error, created_at,
69 started_at, finished_at, updated_at, budget_usd, time_cap_minutes, halted,
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.70 COALESCE((SELECT title FROM pulls WHERE pulls.id = agent_runs.pull_id), agent_runs.title) AS title";
Inbox: the events service tells people what needs them as events arrive71
72#[derive(Deserialize)]
73pub(crate) struct RunRow {
74 id: String,
75 #[allow(dead_code)]
76 workspace: String,
77 #[allow(dead_code)]
78 repo_id: String,
79 repo: String,
80 number: Option<u32>,
81 pub(crate) pull_id: Option<String>,
82 kind: String,
83 agent: String,
84 model: Option<String>,
85 status: String,
86 step: Option<String>,
87 #[serde(default)]
88 steps: Option<String>,
89 step_count: u32,
90 cost_usd: Option<f64>,
91 turns: Option<u32>,
92 token_hash: String,
93 started_by: Option<String>,
94 error: Option<String>,
95 created_at: String,
96 started_at: Option<String>,
97 finished_at: Option<String>,
98 updated_at: String,
99 title: Option<String>,
100 #[serde(default)]
101 budget_usd: Option<f64>,
102 #[serde(default)]
103 time_cap_minutes: Option<u32>,
104 #[serde(default)]
105 halted: Option<String>,
106}
107
108impl RunRow {
109 fn status(&self) -> RunStatus {
110 RunStatus::parse(&self.status).unwrap_or(RunStatus::Failed)
111 }
112
113 /// The run as `member` (or not) may see it: model and cost are the
114 /// workspace's business.
115 fn into_run(self, member: bool) -> AgentRun {
116 let (namespace, name) = self.repo.split_once('/').unwrap_or((&self.repo, ""));
117 AgentRun {
118 repo: RepoPath {
119 namespace: namespace.to_owned(),
120 name: name.to_owned(),
121 },
122 status: self.status(),
123 kind: RunKind::parse(&self.kind).unwrap_or(RunKind::Implement),
124 steps: self
125 .steps
126 .as_deref()
127 .and_then(|steps| serde_json::from_str(steps).ok())
128 .unwrap_or_default(),
129 id: self.id,
130 number: self.number.filter(|number| *number > 0),
131 title: self.title,
132 agent: self.agent,
133 model: self.model.filter(|_| member),
134 step: self.step,
135 step_count: self.step_count,
136 started_by: self.started_by,
137 error: self.error,
138 cost_usd: self.cost_usd.filter(|_| member),
139 turns: self.turns,
140 budget_usd: self.budget_usd.filter(|_| member),
141 time_cap_minutes: self.time_cap_minutes,
142 halted: self.halted,
143 created_at: self.created_at,
144 started_at: self.started_at,
145 finished_at: self.finished_at,
146 updated_at: self.updated_at,
147 }
148 }
149}
150
151/// One line, short enough to read at a glance.
152pub(crate) fn one_line(text: &str, limit: usize) -> String {
153 let line = text.split_whitespace().collect::<Vec<_>>().join(" ");
154 if line.chars().count() <= limit {
155 return line;
156 }
157 let mut short: String = line.chars().take(limit.saturating_sub(1)).collect();
158 short.push('…');
159 short
160}
161
162/// A principal that can read any repository of `namespace`, for lookups a
163/// trusted service asks for. What it is given reads and nothing more.
164pub(crate) fn member_of(actor: &User, namespace: &str) -> Viewer {
165 let mut viewer = actor.clone();
166 let slug = namespace.to_lowercase();
167 if !viewer.is_member(&slug) {
168 viewer.workspaces.push(Membership {
169 base_permission: Some(g1t_contracts::access::BasePermission::Read),
170 ..Membership::member(slug)
171 });
172 }
173 Some(viewer)
174}
175
176/// Whether the viewer belongs to the workspace: what a run spent, its model
177/// and its budget are the workspace's business, not every reader's (an
178/// outside collaborator sees the work, not the bill).
179fn is_member(viewer: &Viewer, namespace: &str) -> bool {
180 viewer
181 .as_ref()
182 .is_some_and(|viewer| viewer.is_member(&namespace.to_lowercase()))
183}
184
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.185#[derive(Deserialize)]
186struct SessionListRow {
187 pull_id: String,
188 number: u32,
189 title: String,
190 status: PullStatus,
191 agent: String,
192 entries: u32,
193 tools: u32,
194 started_at: String,
195 last_at: String,
196 prompt: Option<String>,
197}
198
199#[derive(Deserialize)]
200struct RunSumRow {
201 pull_id: String,
202 kind: String,
203 status: String,
204 cost_usd: Option<f64>,
205}
206
Inbox: the events service tells people what needs them as events arrive207impl Work {
208 /// The repository at `path`, if `viewer` may see it, and whether they
209 /// are a member of its workspace (which shows what runs cost).
210 async fn visible_repo(&self, path: &RepoPath, viewer: &Viewer) -> Result<Outcome<(Repo, bool)>> {
211 Ok(match self.repo(path, viewer).await? {
212 Outcome::Ok(repo) => {
213 let member = is_member(viewer, &repo.namespace);
214 Outcome::Ok((repo, member))
215 }
216 Outcome::Fail(failure) => Outcome::Fail(failure),
217 })
218 }
219
220 async fn run_row(&self, id: &str) -> Result<Option<RunRow>> {
221 self.db
222 .prepare(format!("SELECT {COLUMNS}, steps FROM agent_runs WHERE id = ?"))
223 .bind(&[id.into()])?
224 .first::<RunRow>(None)
225 .await
226 }
227
228 pub(crate) async fn open_run(&self, a: OpenRunArgs) -> Result<Outcome<AgentRunTicket>> {
The work, events, webhooks, actions, repos and packages services no longer run g1t's own agents. Work drops the 35 calls that queued issues for g1t, delegated them, messaged and reviewed through the agent, planned, captured memories automatically, rated confidence and capped agent spend, and its pull request lifecycle no longer revises or reviews: a pull request only advances to a merge by the repository's auto-merge setting. Merging a pull request that is behind now catches it up itself, through the repository's branch update, and lands it when the push arrives; when it can't be caught up cleanly the merge says to merge the base in and try again. Recording a session on a pull request, memory recall and remember, overlaps, the merge queue and required checks stay as they were.229 // Only sandboxes that are not agents are started now; earlier
230 // kinds stay readable on the runs already recorded.
231 if !matches!(a.kind, RunKind::Checks | RunKind::Queue | RunKind::Mergecheck) {
232 return Ok(Outcome::fail(
233 FailureCode::Invalid,
234 format!("A {} run is no longer started.", a.kind.as_str()),
235 ));
236 }
Inbox: the events service tells people what needs them as events arrive237 // The runner is trusted: it names the repository it is starting a
Merge demo dry run fixes: settled checks, other attempts, queue links, clone box, activity feed, DEMO.md238 // sandbox in, whoever the sandbox acts as. The run records where
239 // the repository is now, looked up by its id, not the path it was
240 // named by, which may be from before a transfer or a rename.
241 let (repo_id, path) = match &a.pull_id {
Inbox: the events service tells people what needs them as events arrive242 Some(pull_id) => match self.pull_by_id(pull_id).await? {
Merge demo dry run fixes: settled checks, other attempts, queue links, clone box, activity feed, DEMO.md243 Some(pull) => {
244 let now: Option<RepoPath> = g1t_kit::call(
245 &self.repos,
246 "path_by_id",
247 &g1t_contracts::repos::PathByIdArgs { id: pull.repo_id.clone() },
248 )
249 .await?;
250 (pull.repo_id, now.unwrap_or_else(|| a.repo.clone()))
251 }
Inbox: the events service tells people what needs them as events arrive252 None => return Ok(Outcome::fail(FailureCode::NotFound, "Pull request not found.")),
253 },
254 None => match self.repo(&a.repo, &member_of(&a.actor, &a.repo.namespace)).await? {
Merge demo dry run fixes: settled checks, other attempts, queue links, clone box, activity feed, DEMO.md255 Outcome::Ok(repo) => (repo.id, RepoPath { namespace: repo.namespace, name: repo.name }),
Inbox: the events service tells people what needs them as events arrive256 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
257 },
258 };
Merge demo dry run fixes: settled checks, other attempts, queue links, clone box, activity feed, DEMO.md259 let namespace = path.namespace.to_lowercase();
Inbox: the events service tells people what needs them as events arrive260 // Nothing new starts on an archived or deleted repository.
261 if !self.repo_active(&repo_id).await? {
262 return Ok(Outcome::fail(
263 FailureCode::Forbidden,
Merge demo dry run fixes: settled checks, other attempts, queue links, clone box, activity feed, DEMO.md264 format!("{}/{} is archived or deleted, so nothing new starts on it.", path.namespace, path.name),
Inbox: the events service tells people what needs them as events arrive265 ));
266 }
267 let now = now_ms();
268 let id = new_id("arn", now);
269 let token = new_token();
270 let timestamp = rfc3339(now);
271 let agent = a
272 .agent
273 .clone()
274 .unwrap_or_else(|| g1t_contracts::identity::AGENT_NAME.to_owned());
275 self.db
276 .prepare(
277 "INSERT INTO agent_runs
278 (id, workspace, repo_id, repo, number, pull_id, title, kind, agent, model, status,
279 sandbox, token_hash, started_by, created_at, updated_at, budget_usd, time_cap_minutes)
280 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'queued', ?, ?, ?, ?, ?, ?, ?)",
281 )
282 .bind(&[
283 id.as_str().into(),
284 namespace.into(),
285 repo_id.into(),
Merge demo dry run fixes: settled checks, other attempts, queue links, clone box, activity feed, DEMO.md286 format!("{}/{}", path.namespace, path.name).into(),
Inbox: the events service tells people what needs them as events arrive287 a.number.filter(|n| *n > 0).map_or(JsValue::NULL, JsValue::from),
288 optional(&a.pull_id),
289 optional(&a.title.map(|title| one_line(&title, 200))),
290 a.kind.as_str().into(),
291 agent.into(),
292 optional(&a.model),
293 a.sandbox.into(),
294 hash(&token).into(),
295 optional(&a.started_by),
296 timestamp.as_str().into(),
297 timestamp.as_str().into(),
298 a.budget_usd.filter(|usd| usd.is_finite() && *usd > 0.0).map_or(JsValue::NULL, JsValue::from),
299 a.time_cap_minutes.map_or(JsValue::NULL, JsValue::from),
300 ])?
301 .run()
302 .await?;
303 Ok(Outcome::Ok(AgentRunTicket { run_id: id, token }))
304 }
305
306 /// Adds `text` to a run's steps, keeping the latest `MAX_STEPS`.
307 pub(crate) fn add_step(&self, run_id: &str, at: &str, text: &str) -> Result<worker::D1PreparedStatement> {
308 self.db
309 .prepare(
310 "UPDATE agent_runs SET
311 steps = CASE WHEN json_array_length(steps) >= ?4
312 THEN json_insert(json_remove(steps, '$[0]'), '$[#]', json_object('at', ?1, 'text', ?2))
313 ELSE json_insert(steps, '$[#]', json_object('at', ?1, 'text', ?2)) END,
314 step_count = step_count + 1
315 WHERE id = ?3",
316 )
317 .bind(&[at.into(), text.into(), run_id.into(), MAX_STEPS.into()])
318 }
319
320 pub(crate) async fn report_run(&self, a: ReportRunArgs) -> Result<Outcome<RunStatus>> {
321 let Some(run) = self
322 .run_row(&a.run_id)
323 .await?
324 .filter(|run| run.token_hash == hash(&a.token))
325 else {
326 return Ok(Outcome::fail(FailureCode::NotFound, "Run not found."));
327 };
328 // Finished, or stopped by a person: nothing more is taken, and the
329 // sandbox learns why.
330 if run.finished_at.is_some() {
331 return Ok(Outcome::Ok(run.status()));
332 }
333 // It reached a cap of its guardrails (guardrails.rs).
334 if let Some(halt) = a.halt {
335 let pull_id = run.pull_id.as_deref();
336 return self
337 .halt_run(&run.id, &run.repo_id, pull_id, run.number, &run.kind, halt, a.error, a.cost_usd)
338 .await;
339 }
340 let now = rfc3339(now_ms());
341 let steps: Vec<String> = a
342 .steps
343 .iter()
344 .map(|step| one_line(step, MAX_STEP_CHARS))
345 .filter(|step| !step.is_empty())
346 .collect();
347 // A burst keeps its end: that is where the run is now.
348 let skip = steps.len().saturating_sub(MAX_STEPS_PER_REPORT);
349 let mut statements = Vec::new();
350 for step in &steps[skip..] {
351 statements.push(self.add_step(&run.id, &now, step)?);
352 }
353 let current = a
354 .step
355 .as_deref()
356 .map(|step| one_line(step, MAX_STEP_CHARS))
357 .filter(|step| !step.is_empty())
Merge branch 'run-notes' into fast-push358 .or_else(|| current_step(&steps));
Inbox: the events service tells people what needs them as events arrive359 let outcome = a
360 .outcome
361 .filter(|outcome| matches!(outcome, RunStatus::Succeeded | RunStatus::Failed));
362 let cost = a.cost_usd.filter(|cost| cost.is_finite() && *cost >= 0.0);
363 statements.push(
364 self.db
365 .prepare(
366 "UPDATE agent_runs SET
367 status = COALESCE(?1, CASE WHEN status = 'queued' THEN 'running' ELSE status END),
368 started_at = COALESCE(started_at, ?2),
369 step = COALESCE(?3, step),
370 cost_usd = COALESCE(?4, cost_usd),
371 turns = COALESCE(?5, turns),
372 error = COALESCE(?6, error),
373 finished_at = CASE WHEN ?1 IS NULL THEN finished_at ELSE ?2 END,
374 updated_at = ?2
375 WHERE id = ?7 AND finished_at IS NULL",
376 )
377 .bind(&[
378 outcome.map_or(JsValue::NULL, |outcome| outcome.as_str().into()),
379 now.as_str().into(),
380 optional(&current),
381 cost.map_or(JsValue::NULL, JsValue::from),
382 a.turns.map_or(JsValue::NULL, JsValue::from),
383 optional(&a.error.map(|error| one_line(&error, 1000))),
384 run.id.as_str().into(),
385 ])?,
386 );
387 self.db.batch(statements).await?;
388 Ok(Outcome::Ok(outcome.unwrap_or(RunStatus::Running)))
389 }
390
391 /// Runs whose sandbox died without anyone noticing, marked failed.
392 pub(crate) async fn sweep_silent(&self) -> Result<()> {
393 let now = now_ms();
394 let cutoff = rfc3339(now.saturating_sub(SILENT_HOURS * 3_600_000));
395 self.db
396 .prepare(
397 "UPDATE agent_runs
398 SET status = 'failed', error = COALESCE(error, 'It stopped reporting.'),
399 finished_at = ?, updated_at = ?
400 WHERE status IN ('queued', 'running') AND updated_at < ?",
401 )
402 .bind(&[rfc3339(now).into(), rfc3339(now).into(), cutoff.into()])?
403 .run()
404 .await?;
405 Ok(())
406 }
407
408 pub(crate) async fn list_runs(&self, a: ListRunsArgs) -> Result<Outcome<Vec<AgentRun>>> {
409 let (column, key, member) = if let Some(workspace) = &a.workspace {
410 let slug = workspace.to_lowercase();
411 if !is_member(&a.viewer, &slug) {
412 return Ok(Outcome::fail(
413 FailureCode::Forbidden,
414 "Only members of the workspace can see its fleet.",
415 ));
416 }
417 ("workspace", slug, true)
418 } else if let Some(path) = &a.repo {
419 match self.visible_repo(path, &a.viewer).await? {
420 Outcome::Ok((repo, member)) => ("repo_id", repo.id, member),
421 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
422 }
423 } else {
424 return Ok(Outcome::fail(FailureCode::Invalid, "Name a repository or a workspace."));
425 };
426 self.sweep_silent().await?;
427 let limit = a.limit.unwrap_or(DEFAULT_LIST).clamp(1, MAX_LIST);
428 let rows = self
429 .db
430 .prepare(format!(
431 "SELECT {COLUMNS} FROM agent_runs
432 WHERE {column} = ?1
433 AND (?2 = 0 OR status IN ('queued', 'running'))
434 AND (?3 IS NULL OR kind = ?3)
435 AND (?4 IS NULL OR status = ?4)
436 AND (?5 IS NULL OR number = ?5)
437 ORDER BY created_at DESC LIMIT ?6"
438 ))
439 .bind(&[
440 key.into(),
441 u32::from(a.active).into(),
442 a.kind.map_or(JsValue::NULL, |kind| kind.as_str().into()),
443 a.status.map_or(JsValue::NULL, |status| status.as_str().into()),
444 a.number.map_or(JsValue::NULL, JsValue::from),
445 limit.into(),
446 ])?
447 .all()
448 .await?
449 .results::<RunRow>()?;
450 Ok(Outcome::Ok(rows.into_iter().map(|row| row.into_run(member)).collect()))
451 }
452
453 pub(crate) async fn get_run(&self, a: GetRunArgs) -> Result<Outcome<AgentRun>> {
454 let (repo, member) = match self.visible_repo(&a.repo, &a.viewer).await? {
455 Outcome::Ok(found) => found,
456 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
457 };
458 Ok(match self.run_row(&a.id).await?.filter(|run| run.repo_id == repo.id) {
459 Some(row) => Outcome::Ok(row.into_run(member)),
460 None => Outcome::fail(FailureCode::NotFound, "Run not found."),
461 })
462 }
The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens.463
464 // --- Sessions ----------------------------------------------------------
465
466 pub(crate) async fn list_sessions(&self, a: ListSessionsArgs) -> Result<Outcome<Vec<SessionSummary>>> {
467 let Some(path) = &a.repo else {
468 return Ok(Outcome::fail(FailureCode::Invalid, "Name a repository."));
469 };
470 let (repo, member) = match self.visible_repo(path, &a.viewer).await? {
471 Outcome::Ok(found) => found,
472 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
473 };
474 let rows = self
475 .db
476 .prepare(
477 "SELECT p.id AS pull_id, p.number, p.title, p.status, p.agent,
478 count(*) AS entries,
479 sum(CASE WHEN e.kind = 'tool_call' THEN 1 ELSE 0 END) AS tools,
480 min(e.at) AS started_at, max(e.at) AS last_at,
481 (SELECT substr(f.text, 1, 300) FROM session_entries f
482 WHERE f.pull_id = p.id AND f.kind = 'prompt' ORDER BY f.seq LIMIT 1) AS prompt
483 FROM pulls p JOIN session_entries e ON e.pull_id = p.id
484 WHERE p.repo_id = ?1
485 AND (?2 IS NULL OR p.status = ?2)
486 AND (?3 IS NULL OR p.number = ?3)
487 AND (?4 IS NULL OR EXISTS
488 (SELECT 1 FROM agent_runs r WHERE r.pull_id = p.id AND r.kind = ?4))
489 GROUP BY p.id ORDER BY last_at DESC LIMIT 100",
490 )
491 .bind(&[
492 repo.id.as_str().into(),
493 a.outcome.map_or(JsValue::NULL, |status| status.as_str().into()),
494 a.number.map_or(JsValue::NULL, JsValue::from),
495 a.kind.map_or(JsValue::NULL, |kind| kind.as_str().into()),
496 ])?
497 .all()
498 .await?
499 .results::<SessionListRow>()?;
500 let ids: Vec<&str> = rows.iter().map(|row| row.pull_id.as_str()).collect();
501 let runs = if ids.is_empty() {
502 Vec::new()
503 } else {
504 self.db
505 .prepare(
506 "SELECT pull_id, kind, status, cost_usd FROM agent_runs
507 WHERE repo_id = ? AND pull_id IN (SELECT value FROM json_each(?))",
508 )
509 .bind(&[repo.id.as_str().into(), serde_json::to_string(&ids)?.into()])?
510 .all()
511 .await?
512 .results::<RunSumRow>()?
513 };
514 Ok(Outcome::Ok(
515 rows.into_iter()
516 .map(|row| {
517 let mine: Vec<&RunSumRow> = runs.iter().filter(|run| run.pull_id == row.pull_id).collect();
518 let mut kinds: Vec<RunKind> = Vec::new();
519 for run in &mine {
520 if let Some(kind) = RunKind::parse(&run.kind)
521 && !kinds.contains(&kind)
522 {
523 kinds.push(kind);
524 }
525 }
526 let spent: f64 = mine.iter().filter_map(|run| run.cost_usd).sum();
527 SessionSummary {
528 number: row.number,
529 title: row.title,
530 status: row.status,
531 agent: row.agent,
532 entries: row.entries,
533 tools: row.tools,
534 prompt: row.prompt.map(|prompt| one_line(&prompt, 200)),
535 runs: mine.len() as u32,
536 cost_usd: (member && mine.iter().any(|run| run.cost_usd.is_some())).then_some(spent),
537 active: mine
538 .iter()
539 .any(|run| RunStatus::parse(&run.status).is_some_and(RunStatus::is_active)),
540 kinds,
541 started_at: row.started_at,
542 last_at: row.last_at,
543 }
544 })
545 .collect(),
546 ))
547 }
548
549 pub(crate) async fn get_session(&self, a: GetSessionArgs) -> Result<Outcome<SessionView>> {
550 let (repo, pull) = match self.pull_at(&a.repo, a.number, &a.viewer).await? {
551 Outcome::Ok(found) => found,
552 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
553 };
554 let member = is_member(&a.viewer, &repo.namespace);
555 let entries: Vec<SessionEntry> = self
556 .db
557 .prepare(
558 "SELECT seq, kind, text, tool, \"commit\", at FROM session_entries
559 WHERE pull_id = ? ORDER BY seq LIMIT ?",
560 )
561 .bind(&[pull.id.as_str().into(), SESSION_ENTRIES.into()])?
562 .all()
563 .await?
564 .results::<SessionRow>()?
565 .into_iter()
566 .map(SessionEntry::from)
567 .collect();
568 let runs: Vec<AgentRun> = self
569 .db
570 .prepare(format!(
571 "SELECT {COLUMNS}, steps FROM agent_runs WHERE pull_id = ? ORDER BY created_at DESC LIMIT 50"
572 ))
573 .bind(&[pull.id.as_str().into()])?
574 .all()
575 .await?
576 .results::<RunRow>()?
577 .into_iter()
578 .map(|row| row.into_run(member))
579 .collect();
580 let cost_usd = (member && runs.iter().any(|run| run.cost_usd.is_some()))
581 .then(|| runs.iter().filter_map(|run| run.cost_usd).sum());
582 Ok(Outcome::Ok(SessionView {
583 pull,
584 entries,
585 runs,
586 cost_usd,
587 }))
588 }
Inbox: the events service tells people what needs them as events arrive589}
590
591#[cfg(test)]
592mod tests {
593 use super::*;
594
595 #[test]
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.596 fn transfer_and_deletion_statements_take_what_they_name() {
597 for sql in TRANSFERRED.iter().chain(crate::guardrails::TRANSFERRED) {
598 let n = g1t_kit::transfer::parameters(sql);
599 assert!((1..=5).contains(&n), "{sql}");
600 }
601 for sql in crate::guardrails::DELETED {
602 assert_eq!(g1t_kit::rename::parameters(sql), 1, "{sql}");
603 }
604 }
605
606 #[test]
607 fn rename_statements_take_the_two_slugs() {
608 for sql in RENAMED.iter().chain(crate::guardrails::RENAMED) {
609 assert_eq!(g1t_kit::rename::parameters(sql), 2, "{sql}");
610 }
611 }
612
613 #[test]
Merge branch 'run-notes' into fast-push614 fn a_refused_host_is_noted_but_never_where_the_run_is() {
615 let steps = |list: &[&str]| list.iter().map(|step| (*step).to_owned()).collect::<Vec<String>>();
616 assert_eq!(current_step(&steps(&["Running npm ci", "Blocked: sparrow.cloudflare.com (not an allowed domain)"])).as_deref(), Some("Running npm ci"));
617 assert_eq!(current_step(&steps(&["Blocked: a.com (not an allowed domain)"])), None);
618 assert_eq!(current_step(&steps(&["Blocked: a.com (not an allowed domain)", "Typecheck"])).as_deref(), Some("Typecheck"));
619 assert_eq!(current_step(&[]), None);
620 }
621
622 #[test]
Inbox: the events service tells people what needs them as events arrive623 fn steps_are_one_short_line() {
624 assert_eq!(one_line(" Read\n src/lib.rs ", 40), "Read src/lib.rs");
625 let long = one_line(&"x".repeat(300), 10);
626 assert_eq!(long.chars().count(), 10);
627 assert!(long.ends_with('…'));
628 }
629}

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