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
597 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.
Fixes from the page sweep4//!
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`.
Fixes from the page sweep12
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};
Fixes from the page sweep17use 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;
Fixes from the page sweep25use 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`).
30const NOTE_PREFIXES: [&str; 1] = ["Blocked: "];
31
32/// Where a report leaves the run: its newest step that says what the run
33/// is doing. A note (a refused host) stays in the steps but never stands
34/// for the run, which carries on past it. `None` when there is only notes.
35fn current_step(steps: &[String]) -> Option<String> {
36 steps.iter().rev().find(|step| !NOTE_PREFIXES.iter().any(|prefix| step.starts_with(prefix))).cloned()
37}
38
Fixes from the page sweep39/// The most steps a run keeps; older ones fall off the start.
40const MAX_STEPS: u32 = 200;
41/// The most steps taken from one report.
42const MAX_STEPS_PER_REPORT: usize = 20;
43/// The longest a step is kept.
44const MAX_STEP_CHARS: usize = 240;
45/// A run that has said nothing for this long is taken to have died.
46const SILENT_HOURS: u64 = 3;
47const DEFAULT_LIST: u32 = 50;
48const 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.49/// The most session entries one view returns.
50const SESSION_ENTRIES: u32 = 2000;
Fixes from the page sweep51
52/// Every column but the steps, which only a run's own page needs.
53const COLUMNS: &str = "id, workspace, repo_id, repo, number, pull_id, kind, agent, model, status,
54 step, step_count, cost_usd, turns, sandbox, token_hash, started_by, error, created_at,
55 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.56 COALESCE((SELECT title FROM pulls WHERE pulls.id = agent_runs.pull_id), agent_runs.title) AS title";
Fixes from the page sweep57
58#[derive(Deserialize)]
59pub(crate) struct RunRow {
60 id: String,
61 #[allow(dead_code)]
62 workspace: String,
63 #[allow(dead_code)]
64 repo_id: String,
65 repo: String,
66 number: Option<u32>,
67 pub(crate) pull_id: Option<String>,
68 kind: String,
69 agent: String,
70 model: Option<String>,
71 status: String,
72 step: Option<String>,
73 #[serde(default)]
74 steps: Option<String>,
75 step_count: u32,
76 cost_usd: Option<f64>,
77 turns: Option<u32>,
78 token_hash: String,
79 started_by: Option<String>,
80 error: Option<String>,
81 created_at: String,
82 started_at: Option<String>,
83 finished_at: Option<String>,
84 updated_at: String,
85 title: Option<String>,
86 #[serde(default)]
87 budget_usd: Option<f64>,
88 #[serde(default)]
89 time_cap_minutes: Option<u32>,
90 #[serde(default)]
91 halted: Option<String>,
92}
93
94impl RunRow {
95 fn status(&self) -> RunStatus {
96 RunStatus::parse(&self.status).unwrap_or(RunStatus::Failed)
97 }
98
99 /// The run as `member` (or not) may see it: model and cost are the
100 /// workspace's business.
101 fn into_run(self, member: bool) -> AgentRun {
102 let (namespace, name) = self.repo.split_once('/').unwrap_or((&self.repo, ""));
103 AgentRun {
104 repo: RepoPath {
105 namespace: namespace.to_owned(),
106 name: name.to_owned(),
107 },
108 status: self.status(),
109 kind: RunKind::parse(&self.kind).unwrap_or(RunKind::Implement),
110 steps: self
111 .steps
112 .as_deref()
113 .and_then(|steps| serde_json::from_str(steps).ok())
114 .unwrap_or_default(),
115 id: self.id,
116 number: self.number.filter(|number| *number > 0),
117 title: self.title,
118 agent: self.agent,
119 model: self.model.filter(|_| member),
120 step: self.step,
121 step_count: self.step_count,
122 started_by: self.started_by,
123 error: self.error,
124 cost_usd: self.cost_usd.filter(|_| member),
125 turns: self.turns,
126 budget_usd: self.budget_usd.filter(|_| member),
127 time_cap_minutes: self.time_cap_minutes,
128 halted: self.halted,
129 created_at: self.created_at,
130 started_at: self.started_at,
131 finished_at: self.finished_at,
132 updated_at: self.updated_at,
133 }
134 }
135}
136
137/// One line, short enough to read at a glance.
138pub(crate) fn one_line(text: &str, limit: usize) -> String {
139 let line = text.split_whitespace().collect::<Vec<_>>().join(" ");
140 if line.chars().count() <= limit {
141 return line;
142 }
143 let mut short: String = line.chars().take(limit.saturating_sub(1)).collect();
144 short.push('…');
145 short
146}
147
148/// A principal that can read any repository of `namespace`, for lookups a
149/// trusted service asks for. What it is given reads and nothing more.
150pub(crate) fn member_of(actor: &User, namespace: &str) -> Viewer {
151 let mut viewer = actor.clone();
152 let slug = namespace.to_lowercase();
153 if !viewer.is_member(&slug) {
154 viewer.workspaces.push(Membership {
155 base_permission: Some(g1t_contracts::access::BasePermission::Read),
156 ..Membership::member(slug)
157 });
158 }
159 Some(viewer)
160}
161
162/// Whether the viewer belongs to the workspace: what a run spent, its model
163/// and its budget are the workspace's business, not every reader's (an
164/// outside collaborator sees the work, not the bill).
165fn is_member(viewer: &Viewer, namespace: &str) -> bool {
166 viewer
167 .as_ref()
168 .is_some_and(|viewer| viewer.is_member(&namespace.to_lowercase()))
169}
170
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.171#[derive(Deserialize)]
172struct SessionListRow {
173 pull_id: String,
174 number: u32,
175 title: String,
176 status: PullStatus,
177 agent: String,
178 entries: u32,
179 tools: u32,
180 started_at: String,
181 last_at: String,
182 prompt: Option<String>,
183}
184
185#[derive(Deserialize)]
186struct RunSumRow {
187 pull_id: String,
188 kind: String,
189 status: String,
190 cost_usd: Option<f64>,
191}
192
Fixes from the page sweep193impl Work {
194 /// The repository at `path`, if `viewer` may see it, and whether they
195 /// are a member of its workspace (which shows what runs cost).
196 async fn visible_repo(&self, path: &RepoPath, viewer: &Viewer) -> Result<Outcome<(Repo, bool)>> {
197 Ok(match self.repo(path, viewer).await? {
198 Outcome::Ok(repo) => {
199 let member = is_member(viewer, &repo.namespace);
200 Outcome::Ok((repo, member))
201 }
202 Outcome::Fail(failure) => Outcome::Fail(failure),
203 })
204 }
205
206 async fn run_row(&self, id: &str) -> Result<Option<RunRow>> {
207 self.db
208 .prepare(format!("SELECT {COLUMNS}, steps FROM agent_runs WHERE id = ?"))
209 .bind(&[id.into()])?
210 .first::<RunRow>(None)
211 .await
212 }
213
214 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.215 // Only sandboxes that are not agents are started now; earlier
216 // kinds stay readable on the runs already recorded.
217 if !matches!(a.kind, RunKind::Checks | RunKind::Queue | RunKind::Mergecheck) {
218 return Ok(Outcome::fail(
219 FailureCode::Invalid,
220 format!("A {} run is no longer started.", a.kind.as_str()),
221 ));
222 }
Fixes from the page sweep223 // 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.md224 // sandbox in, whoever the sandbox acts as. The run records where
225 // the repository is now, looked up by its id, not the path it was
226 // named by, which may be from before a transfer or a rename.
227 let (repo_id, path) = match &a.pull_id {
Fixes from the page sweep228 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.md229 Some(pull) => {
230 let now: Option<RepoPath> = g1t_kit::call(
231 &self.repos,
232 "path_by_id",
233 &g1t_contracts::repos::PathByIdArgs { id: pull.repo_id.clone() },
234 )
235 .await?;
236 (pull.repo_id, now.unwrap_or_else(|| a.repo.clone()))
237 }
Fixes from the page sweep238 None => return Ok(Outcome::fail(FailureCode::NotFound, "Pull request not found.")),
239 },
240 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.md241 Outcome::Ok(repo) => (repo.id, RepoPath { namespace: repo.namespace, name: repo.name }),
Fixes from the page sweep242 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
243 },
244 };
Merge demo dry run fixes: settled checks, other attempts, queue links, clone box, activity feed, DEMO.md245 let namespace = path.namespace.to_lowercase();
Fixes from the page sweep246 // Nothing new starts on an archived or deleted repository.
247 if !self.repo_active(&repo_id).await? {
248 return Ok(Outcome::fail(
249 FailureCode::Forbidden,
Merge demo dry run fixes: settled checks, other attempts, queue links, clone box, activity feed, DEMO.md250 format!("{}/{} is archived or deleted, so nothing new starts on it.", path.namespace, path.name),
Fixes from the page sweep251 ));
252 }
253 let now = now_ms();
254 let id = new_id("arn", now);
255 let token = new_token();
256 let timestamp = rfc3339(now);
257 let agent = a
258 .agent
259 .clone()
260 .unwrap_or_else(|| g1t_contracts::identity::AGENT_NAME.to_owned());
261 self.db
262 .prepare(
263 "INSERT INTO agent_runs
264 (id, workspace, repo_id, repo, number, pull_id, title, kind, agent, model, status,
265 sandbox, token_hash, started_by, created_at, updated_at, budget_usd, time_cap_minutes)
266 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'queued', ?, ?, ?, ?, ?, ?, ?)",
267 )
268 .bind(&[
269 id.as_str().into(),
270 namespace.into(),
271 repo_id.into(),
Merge demo dry run fixes: settled checks, other attempts, queue links, clone box, activity feed, DEMO.md272 format!("{}/{}", path.namespace, path.name).into(),
Fixes from the page sweep273 a.number.filter(|n| *n > 0).map_or(JsValue::NULL, JsValue::from),
274 optional(&a.pull_id),
275 optional(&a.title.map(|title| one_line(&title, 200))),
276 a.kind.as_str().into(),
277 agent.into(),
278 optional(&a.model),
279 a.sandbox.into(),
280 hash(&token).into(),
281 optional(&a.started_by),
282 timestamp.as_str().into(),
283 timestamp.as_str().into(),
284 a.budget_usd.filter(|usd| usd.is_finite() && *usd > 0.0).map_or(JsValue::NULL, JsValue::from),
285 a.time_cap_minutes.map_or(JsValue::NULL, JsValue::from),
286 ])?
287 .run()
288 .await?;
289 Ok(Outcome::Ok(AgentRunTicket { run_id: id, token }))
290 }
291
292 /// Adds `text` to a run's steps, keeping the latest `MAX_STEPS`.
293 pub(crate) fn add_step(&self, run_id: &str, at: &str, text: &str) -> Result<worker::D1PreparedStatement> {
294 self.db
295 .prepare(
296 "UPDATE agent_runs SET
297 steps = CASE WHEN json_array_length(steps) >= ?4
298 THEN json_insert(json_remove(steps, '$[0]'), '$[#]', json_object('at', ?1, 'text', ?2))
299 ELSE json_insert(steps, '$[#]', json_object('at', ?1, 'text', ?2)) END,
300 step_count = step_count + 1
301 WHERE id = ?3",
302 )
303 .bind(&[at.into(), text.into(), run_id.into(), MAX_STEPS.into()])
304 }
305
306 pub(crate) async fn report_run(&self, a: ReportRunArgs) -> Result<Outcome<RunStatus>> {
307 let Some(run) = self
308 .run_row(&a.run_id)
309 .await?
310 .filter(|run| run.token_hash == hash(&a.token))
311 else {
312 return Ok(Outcome::fail(FailureCode::NotFound, "Run not found."));
313 };
314 // Finished, or stopped by a person: nothing more is taken, and the
315 // sandbox learns why.
316 if run.finished_at.is_some() {
317 return Ok(Outcome::Ok(run.status()));
318 }
319 // It reached a cap of its guardrails (guardrails.rs).
320 if let Some(halt) = a.halt {
321 let pull_id = run.pull_id.as_deref();
322 return self
323 .halt_run(&run.id, &run.repo_id, pull_id, run.number, &run.kind, halt, a.error, a.cost_usd)
324 .await;
325 }
326 let now = rfc3339(now_ms());
327 let steps: Vec<String> = a
328 .steps
329 .iter()
330 .map(|step| one_line(step, MAX_STEP_CHARS))
331 .filter(|step| !step.is_empty())
332 .collect();
333 // A burst keeps its end: that is where the run is now.
334 let skip = steps.len().saturating_sub(MAX_STEPS_PER_REPORT);
335 let mut statements = Vec::new();
336 for step in &steps[skip..] {
337 statements.push(self.add_step(&run.id, &now, step)?);
338 }
339 let current = a
340 .step
341 .as_deref()
342 .map(|step| one_line(step, MAX_STEP_CHARS))
343 .filter(|step| !step.is_empty())
Merge branch 'run-notes' into fast-push344 .or_else(|| current_step(&steps));
Fixes from the page sweep345 let outcome = a
346 .outcome
347 .filter(|outcome| matches!(outcome, RunStatus::Succeeded | RunStatus::Failed));
348 let cost = a.cost_usd.filter(|cost| cost.is_finite() && *cost >= 0.0);
349 statements.push(
350 self.db
351 .prepare(
352 "UPDATE agent_runs SET
353 status = COALESCE(?1, CASE WHEN status = 'queued' THEN 'running' ELSE status END),
354 started_at = COALESCE(started_at, ?2),
355 step = COALESCE(?3, step),
356 cost_usd = COALESCE(?4, cost_usd),
357 turns = COALESCE(?5, turns),
358 error = COALESCE(?6, error),
359 finished_at = CASE WHEN ?1 IS NULL THEN finished_at ELSE ?2 END,
360 updated_at = ?2
361 WHERE id = ?7 AND finished_at IS NULL",
362 )
363 .bind(&[
364 outcome.map_or(JsValue::NULL, |outcome| outcome.as_str().into()),
365 now.as_str().into(),
366 optional(&current),
367 cost.map_or(JsValue::NULL, JsValue::from),
368 a.turns.map_or(JsValue::NULL, JsValue::from),
369 optional(&a.error.map(|error| one_line(&error, 1000))),
370 run.id.as_str().into(),
371 ])?,
372 );
373 self.db.batch(statements).await?;
374 Ok(Outcome::Ok(outcome.unwrap_or(RunStatus::Running)))
375 }
376
377 /// Runs whose sandbox died without anyone noticing, marked failed.
378 pub(crate) async fn sweep_silent(&self) -> Result<()> {
379 let now = now_ms();
380 let cutoff = rfc3339(now.saturating_sub(SILENT_HOURS * 3_600_000));
381 self.db
382 .prepare(
383 "UPDATE agent_runs
384 SET status = 'failed', error = COALESCE(error, 'It stopped reporting.'),
385 finished_at = ?, updated_at = ?
386 WHERE status IN ('queued', 'running') AND updated_at < ?",
387 )
388 .bind(&[rfc3339(now).into(), rfc3339(now).into(), cutoff.into()])?
389 .run()
390 .await?;
391 Ok(())
392 }
393
394 pub(crate) async fn list_runs(&self, a: ListRunsArgs) -> Result<Outcome<Vec<AgentRun>>> {
395 let (column, key, member) = if let Some(workspace) = &a.workspace {
396 let slug = workspace.to_lowercase();
397 if !is_member(&a.viewer, &slug) {
398 return Ok(Outcome::fail(
399 FailureCode::Forbidden,
400 "Only members of the workspace can see its fleet.",
401 ));
402 }
403 ("workspace", slug, true)
404 } else if let Some(path) = &a.repo {
405 match self.visible_repo(path, &a.viewer).await? {
406 Outcome::Ok((repo, member)) => ("repo_id", repo.id, member),
407 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
408 }
409 } else {
410 return Ok(Outcome::fail(FailureCode::Invalid, "Name a repository or a workspace."));
411 };
412 self.sweep_silent().await?;
413 let limit = a.limit.unwrap_or(DEFAULT_LIST).clamp(1, MAX_LIST);
414 let rows = self
415 .db
416 .prepare(format!(
417 "SELECT {COLUMNS} FROM agent_runs
418 WHERE {column} = ?1
419 AND (?2 = 0 OR status IN ('queued', 'running'))
420 AND (?3 IS NULL OR kind = ?3)
421 AND (?4 IS NULL OR status = ?4)
422 AND (?5 IS NULL OR number = ?5)
423 ORDER BY created_at DESC LIMIT ?6"
424 ))
425 .bind(&[
426 key.into(),
427 u32::from(a.active).into(),
428 a.kind.map_or(JsValue::NULL, |kind| kind.as_str().into()),
429 a.status.map_or(JsValue::NULL, |status| status.as_str().into()),
430 a.number.map_or(JsValue::NULL, JsValue::from),
431 limit.into(),
432 ])?
433 .all()
434 .await?
435 .results::<RunRow>()?;
436 Ok(Outcome::Ok(rows.into_iter().map(|row| row.into_run(member)).collect()))
437 }
438
439 pub(crate) async fn get_run(&self, a: GetRunArgs) -> Result<Outcome<AgentRun>> {
440 let (repo, member) = match self.visible_repo(&a.repo, &a.viewer).await? {
441 Outcome::Ok(found) => found,
442 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
443 };
444 Ok(match self.run_row(&a.id).await?.filter(|run| run.repo_id == repo.id) {
445 Some(row) => Outcome::Ok(row.into_run(member)),
446 None => Outcome::fail(FailureCode::NotFound, "Run not found."),
447 })
448 }
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.449
450 // --- Sessions ----------------------------------------------------------
451
452 pub(crate) async fn list_sessions(&self, a: ListSessionsArgs) -> Result<Outcome<Vec<SessionSummary>>> {
453 let Some(path) = &a.repo else {
454 return Ok(Outcome::fail(FailureCode::Invalid, "Name a repository."));
455 };
456 let (repo, member) = match self.visible_repo(path, &a.viewer).await? {
457 Outcome::Ok(found) => found,
458 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
459 };
460 let rows = self
461 .db
462 .prepare(
463 "SELECT p.id AS pull_id, p.number, p.title, p.status, p.agent,
464 count(*) AS entries,
465 sum(CASE WHEN e.kind = 'tool_call' THEN 1 ELSE 0 END) AS tools,
466 min(e.at) AS started_at, max(e.at) AS last_at,
467 (SELECT substr(f.text, 1, 300) FROM session_entries f
468 WHERE f.pull_id = p.id AND f.kind = 'prompt' ORDER BY f.seq LIMIT 1) AS prompt
469 FROM pulls p JOIN session_entries e ON e.pull_id = p.id
470 WHERE p.repo_id = ?1
471 AND (?2 IS NULL OR p.status = ?2)
472 AND (?3 IS NULL OR p.number = ?3)
473 AND (?4 IS NULL OR EXISTS
474 (SELECT 1 FROM agent_runs r WHERE r.pull_id = p.id AND r.kind = ?4))
475 GROUP BY p.id ORDER BY last_at DESC LIMIT 100",
476 )
477 .bind(&[
478 repo.id.as_str().into(),
479 a.outcome.map_or(JsValue::NULL, |status| status.as_str().into()),
480 a.number.map_or(JsValue::NULL, JsValue::from),
481 a.kind.map_or(JsValue::NULL, |kind| kind.as_str().into()),
482 ])?
483 .all()
484 .await?
485 .results::<SessionListRow>()?;
486 let ids: Vec<&str> = rows.iter().map(|row| row.pull_id.as_str()).collect();
487 let runs = if ids.is_empty() {
488 Vec::new()
489 } else {
490 self.db
491 .prepare(
492 "SELECT pull_id, kind, status, cost_usd FROM agent_runs
493 WHERE repo_id = ? AND pull_id IN (SELECT value FROM json_each(?))",
494 )
495 .bind(&[repo.id.as_str().into(), serde_json::to_string(&ids)?.into()])?
496 .all()
497 .await?
498 .results::<RunSumRow>()?
499 };
500 Ok(Outcome::Ok(
501 rows.into_iter()
502 .map(|row| {
503 let mine: Vec<&RunSumRow> = runs.iter().filter(|run| run.pull_id == row.pull_id).collect();
504 let mut kinds: Vec<RunKind> = Vec::new();
505 for run in &mine {
506 if let Some(kind) = RunKind::parse(&run.kind)
507 && !kinds.contains(&kind)
508 {
509 kinds.push(kind);
510 }
511 }
512 let spent: f64 = mine.iter().filter_map(|run| run.cost_usd).sum();
513 SessionSummary {
514 number: row.number,
515 title: row.title,
516 status: row.status,
517 agent: row.agent,
518 entries: row.entries,
519 tools: row.tools,
520 prompt: row.prompt.map(|prompt| one_line(&prompt, 200)),
521 runs: mine.len() as u32,
522 cost_usd: (member && mine.iter().any(|run| run.cost_usd.is_some())).then_some(spent),
523 active: mine
524 .iter()
525 .any(|run| RunStatus::parse(&run.status).is_some_and(RunStatus::is_active)),
526 kinds,
527 started_at: row.started_at,
528 last_at: row.last_at,
529 }
530 })
531 .collect(),
532 ))
533 }
534
535 pub(crate) async fn get_session(&self, a: GetSessionArgs) -> Result<Outcome<SessionView>> {
536 let (repo, pull) = match self.pull_at(&a.repo, a.number, &a.viewer).await? {
537 Outcome::Ok(found) => found,
538 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
539 };
540 let member = is_member(&a.viewer, &repo.namespace);
541 let entries: Vec<SessionEntry> = self
542 .db
543 .prepare(
544 "SELECT seq, kind, text, tool, \"commit\", at FROM session_entries
545 WHERE pull_id = ? ORDER BY seq LIMIT ?",
546 )
547 .bind(&[pull.id.as_str().into(), SESSION_ENTRIES.into()])?
548 .all()
549 .await?
550 .results::<SessionRow>()?
551 .into_iter()
552 .map(SessionEntry::from)
553 .collect();
554 let runs: Vec<AgentRun> = self
555 .db
556 .prepare(format!(
557 "SELECT {COLUMNS}, steps FROM agent_runs WHERE pull_id = ? ORDER BY created_at DESC LIMIT 50"
558 ))
559 .bind(&[pull.id.as_str().into()])?
560 .all()
561 .await?
562 .results::<RunRow>()?
563 .into_iter()
564 .map(|row| row.into_run(member))
565 .collect();
566 let cost_usd = (member && runs.iter().any(|run| run.cost_usd.is_some()))
567 .then(|| runs.iter().filter_map(|run| run.cost_usd).sum());
568 Ok(Outcome::Ok(SessionView {
569 pull,
570 entries,
571 runs,
572 cost_usd,
573 }))
574 }
Fixes from the page sweep575}
576
577#[cfg(test)]
578mod tests {
579 use super::*;
580
581 #[test]
Merge branch 'run-notes' into fast-push582 fn a_refused_host_is_noted_but_never_where_the_run_is() {
583 let steps = |list: &[&str]| list.iter().map(|step| (*step).to_owned()).collect::<Vec<String>>();
584 assert_eq!(current_step(&steps(&["Running npm ci", "Blocked: sparrow.cloudflare.com (not an allowed domain)"])).as_deref(), Some("Running npm ci"));
585 assert_eq!(current_step(&steps(&["Blocked: a.com (not an allowed domain)"])), None);
586 assert_eq!(current_step(&steps(&["Blocked: a.com (not an allowed domain)", "Typecheck"])).as_deref(), Some("Typecheck"));
587 assert_eq!(current_step(&[]), None);
588 }
589
590 #[test]
Fixes from the page sweep591 fn steps_are_one_short_line() {
592 assert_eq!(one_line(" Read\n src/lib.rs ", 40), "Read src/lib.rs");
593 let long = one_line(&"x".repeat(300), 10);
594 assert_eq!(long.chars().count(), 10);
595 assert!(long.ends_with('…'));
596 }
597}

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