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
555 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.

A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait1//! Memory: what agents and people have learned that the next agent should
2//! know, at two levels. A project's memory is about its codebase; the
3//! workspace's is true across its projects ("we use pnpm everywhere",
4//! "staging lives at …").
5//!
6//! A workspace's memory is members-only, since it can hold internal
7//! knowledge; a project's is for whoever can read the project, and changed
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//! by whoever can push to it. It never holds a secret: text that looks like a key or a token is refused.
9//! Agents read it with `recall` and add to it with `remember` (MCP), and
10//! people manage it on the Memory pages.
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait11
12use g1t_contracts::access::{self, Capability};
13use g1t_contracts::agents::*;
14use g1t_contracts::repos::{Repo, RepoPath};
15use g1t_contracts::time::rfc3339;
16use g1t_contracts::{FailureCode, Outcome, PrincipalKind, User, Viewer, new_id};
17use g1t_kit::now_ms;
18use serde::Deserialize;
19use worker::Result;
20use worker::wasm_bindgen::JsValue;
21
22use crate::Work;
23
24/// A workspace renamed: its runs and memories move to the slug it has now.
25/// `?1` is the current slug, `?2` a stale one (see `g1t_kit::rename`).
26pub(crate) const RENAMED: &[&str] = &[
27 "UPDATE agent_runs SET workspace = ?1 WHERE workspace = ?2",
28 "UPDATE agent_runs SET repo = ?1 || substr(repo, length(?2) + 1) WHERE substr(repo, 1, length(?2) + 1) = ?2 || '/'",
29 "UPDATE memories SET scope_key = ?1 WHERE scope = 'workspace' AND scope_key = ?2",
30 "UPDATE memories SET workspace = ?1 WHERE workspace = ?2",
31 "UPDATE memories SET repo = ?1 || substr(repo, length(?2) + 1) WHERE substr(repo, 1, length(?2) + 1) = ?2 || '/'",
32 "UPDATE memories SET source_repo = ?1 || substr(source_repo, length(?2) + 1) WHERE substr(source_repo, 1, length(?2) + 1) = ?2 || '/'",
33];
34
35/// A repository transferred (see `g1t_kit::transfer`): its agent runs and
36/// its project's memory go with it, as its issues and pull requests do by
37/// id. `?1`/`?2` the path now and before, `?3`/`?4` the workspaces, `?5`
38/// the repository's id.
39pub(crate) const TRANSFERRED: &[&str] = &[
40 "UPDATE agent_runs SET workspace = ?3, repo = ?1 WHERE repo_id = ?5 AND repo = ?2",
41 "UPDATE memories SET workspace = ?3, repo = ?1 WHERE scope = 'project' AND scope_key = ?5 AND workspace = ?4",
42 "UPDATE memories SET source_repo = ?1 WHERE source_repo = ?2",
43];
44
45/// A workspace deleted: its own memory, guardrails and queued runs go.
46/// `?1` is its slug. Project memory went with each repository's transfer.
47pub(crate) const DELETED: &[&str] = &[
48 "DELETE FROM memories WHERE scope = 'workspace' AND scope_key = ?1",
49 "DELETE FROM guardrails WHERE scope = 'workspace' AND scope_key = ?1",
50];
51
52/// The most memories one project, or one workspace, keeps.
53const MAX_PER_SCOPE: u32 = 500;
54const DEFAULT_RECALL: u32 = 20;
55
56#[derive(Deserialize)]
57pub(crate) struct MemoryRow {
58 id: String,
59 pub(crate) scope: String,
60 workspace: String,
61 repo: Option<String>,
62 text: String,
63 kind: String,
64 source_kind: String,
65 source_run: Option<String>,
66 source_repo: Option<String>,
67 source_number: Option<u32>,
68 created_by: String,
69 pinned: u32,
70 created_at: String,
71 updated_at: String,
72 last_used_at: Option<String>,
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.73 // What automatic capture added (migration 0019); read for memories
74 // kept from before it was removed.
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait75 #[serde(default)]
76 status: Option<String>,
77 #[serde(default)]
78 confidence: Option<f64>,
79 #[serde(default)]
80 source_ref: Option<String>,
81 #[serde(default)]
82 evidence: Option<String>,
83 #[serde(default)]
84 sources: Option<String>,
85}
86
87fn path(text: &str) -> Option<RepoPath> {
88 let (namespace, name) = text.split_once('/')?;
89 Some(RepoPath {
90 namespace: namespace.to_owned(),
91 name: name.to_owned(),
92 })
93}
94
95impl From<MemoryRow> for Memory {
96 fn from(row: MemoryRow) -> Self {
97 Memory {
98 id: row.id,
99 scope: if row.scope == "workspace" {
100 MemoryScope::Workspace
101 } else {
102 MemoryScope::Project
103 },
104 workspace: row.workspace,
105 repo: row.repo.as_deref().and_then(path),
106 text: row.text,
107 kind: MemoryKind::parse(&row.kind).unwrap_or_default(),
108 source: MemorySource {
109 kind: row.source_kind,
110 run_id: row.source_run,
111 repo: row.source_repo.as_deref().and_then(path),
112 number: row.source_number,
113 reference: row.source_ref,
114 evidence: row.evidence,
115 },
116 created_by: row.created_by,
117 pinned: row.pinned != 0,
118 created_at: row.created_at,
119 updated_at: row.updated_at,
120 last_used_at: row.last_used_at,
121 status: row.status.as_deref().and_then(MemoryStatus::parse).unwrap_or_default(),
122 confidence: row.confidence,
123 seen: row
124 .sources
125 .as_deref()
126 .and_then(|sources| serde_json::from_str::<Vec<String>>(sources).ok())
127 .map_or(1, |sources| sources.len().max(1) as u32),
128 }
129 }
130}
131
132/// The text tidied, or why it cannot be kept.
133fn valid_text(text: &str) -> std::result::Result<String, String> {
134 let text = text.trim();
135 if text.is_empty() {
136 return Err("Write what should be remembered.".into());
137 }
138 if text.chars().count() > MAX_MEMORY_CHARS {
139 return Err(format!(
140 "Keep a memory to {MAX_MEMORY_CHARS} characters: one fact, convention, decision or gotcha each."
141 ));
142 }
143 if let Some(what) = secret_in(text) {
144 return Err(format!(
145 "That looks like {what}. Memory is read by every agent in the workspace, so it never holds secrets. Say where the secret lives instead, such as \"the deploy key is the DEPLOY_KEY secret\"."
146 ));
147 }
148 Ok(text.to_owned())
149}
150
151/// Why a person cannot change a memory.
152const CHANGE_DENIED: &str = "Only members of the workspace can change its memory, and a project's only with the Write role on it.";
153
154fn denied<T>() -> Outcome<T> {
155 Outcome::fail(
156 FailureCode::Forbidden,
157 "Memory is for members of the workspace and its agents.",
158 )
159}
160
161/// Whether `actor` may change a memory of `workspace`: the workspace's own
162/// needs a member; a project's (`repo_id`, its repository) the Write role
163/// on that repository, as pushing to it does.
164fn may_write(actor: &User, workspace: &str, repo_id: Option<&str>) -> bool {
165 let able = actor.verified || actor.kind != PrincipalKind::User;
166 able && match repo_id {
167 None => actor.is_member(workspace),
168 // Write is never had through being public, so treating it as
169 // private changes nothing.
170 Some(id) => access::can(
171 Some(actor),
172 access::RepoRef { id, namespace: workspace, private: true },
173 Capability::Push,
174 ),
175 }
176}
177
178/// The order memories are given and listed in: pinned, then most recently
179/// used or changed.
180const ORDER: &str = "pinned DESC, COALESCE(last_used_at, updated_at) DESC, created_at DESC";
181
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.182/// A memory's text folded for comparing: lowercase words and digits, one
183/// space apart, so wording that differs only in case, punctuation or
184/// spacing is the same memory.
185fn fingerprint(text: &str) -> String {
186 text.to_lowercase()
187 .split(|c: char| !c.is_alphanumeric())
188 .filter(|word| !word.is_empty())
189 .collect::<Vec<_>>()
190 .join(" ")
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait191}
192
193impl Work {
194 async fn memories_of(&self, scope: MemoryScope, key: &str, limit: u32) -> Result<Vec<Memory>> {
195 Ok(self
196 .db
197 .prepare(format!(
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.198 // Only what is kept: candidates captured before are not shown.
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait199 "SELECT * FROM memories WHERE scope = ? AND scope_key = ? AND status = 'kept' ORDER BY {ORDER} LIMIT ?"
200 ))
201 .bind(&[scope.as_str().into(), key.into(), limit.into()])?
202 .all()
203 .await?
204 .results::<MemoryRow>()?
205 .into_iter()
206 .map(Memory::from)
207 .collect())
208 }
209
210 async fn memory_row(&self, workspace: &str, id: &str) -> Result<Option<Memory>> {
211 Ok(self
212 .db
213 .prepare("SELECT * FROM memories WHERE id = ? AND workspace = ?")
214 .bind(&[id.into(), workspace.into()])?
215 .first::<MemoryRow>(None)
216 .await?
217 .map(Memory::from))
218 }
219
220 /// The repository a project's memory is about; none for the workspace's.
221 async fn memory_repo_id(&self, id: &str) -> Result<Option<String>> {
222 self
223 .db
224 .prepare("SELECT scope_key AS value FROM memories WHERE id = ? AND scope = 'project'")
225 .bind(&[id.into()])?
226 .first::<String>(Some("value"))
227 .await
228 }
229
230 async fn mark_used(&self, ids: &[String]) -> Result<()> {
231 if ids.is_empty() {
232 return Ok(());
233 }
234 self.db
235 .prepare("UPDATE memories SET last_used_at = ? WHERE id IN (SELECT value FROM json_each(?))")
236 .bind(&[rfc3339(now_ms()).into(), serde_json::to_string(ids)?.into()])?
237 .run()
238 .await?;
239 Ok(())
240 }
241
242 /// The project's repository, which must be in `workspace`.
243 async fn project_repo(&self, path: &RepoPath, viewer: &Viewer, workspace: &str) -> Result<Outcome<Repo>> {
244 Ok(match self.repo(path, viewer).await? {
245 Outcome::Ok(repo) if repo.namespace.to_lowercase() == workspace => Outcome::Ok(repo),
246 Outcome::Ok(_) => Outcome::fail(FailureCode::Invalid, "That project is in another workspace."),
247 Outcome::Fail(failure) => Outcome::Fail(failure),
248 })
249 }
250
251 pub(crate) async fn list_memories(&self, a: ListMemoriesArgs) -> Result<Outcome<Memories>> {
252 let workspace = a.workspace.to_lowercase();
253 let member = a.viewer.as_ref().is_some_and(|viewer| viewer.is_member(&workspace));
254 // Someone else who can read the project sees its memory alone.
255 if !member && a.repo.is_none() {
256 return Ok(denied());
257 }
258 let project = match &a.repo {
259 Some(path) => match self.project_repo(path, &a.viewer, &workspace).await? {
260 Outcome::Ok(repo) => self.memories_of(MemoryScope::Project, &repo.id, MAX_PER_SCOPE).await?,
261 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
262 },
263 None => Vec::new(),
264 };
265 let workspace = if member {
266 self.memories_of(MemoryScope::Workspace, &workspace, MAX_PER_SCOPE).await?
267 } else {
268 Vec::new()
269 };
270 Ok(Outcome::Ok(Memories { project, workspace }))
271 }
272
273 pub(crate) async fn add_memory(&self, a: AddMemoryArgs) -> Result<Outcome<Memory>> {
274 let workspace = a.workspace.to_lowercase();
275 if a.scope == MemoryScope::Workspace && !may_write(&a.actor, &workspace, None) {
276 return Ok(denied());
277 }
278 let text = match valid_text(&a.text) {
279 Ok(text) => text,
280 Err(message) => return Ok(Outcome::fail(FailureCode::Invalid, message)),
281 };
282 let viewer = Some(a.actor.clone());
283 let repo = match &a.repo {
284 Some(path) => match self.project_repo(path, &viewer, &workspace).await? {
285 Outcome::Ok(repo) => Some(repo),
286 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
287 },
288 None => None,
289 };
290 let key = match (a.scope, &repo) {
291 (MemoryScope::Workspace, _) => workspace.clone(),
292 (MemoryScope::Project, Some(repo)) => {
293 if !may_write(&a.actor, &workspace, Some(&repo.id)) {
294 return Ok(Outcome::fail(FailureCode::Forbidden, CHANGE_DENIED));
295 }
296 repo.id.clone()
297 }
298 (MemoryScope::Project, None) => {
299 return Ok(Outcome::fail(FailureCode::Invalid, "Name the project this memory is about."));
300 }
301 };
302 // The same thing remembered twice is one memory, freshened; written
303 // by hand, a candidate or a dismissed one with the same words is
304 // kept.
305 let now = rfc3339(now_ms());
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.306 let print = fingerprint(&text);
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait307 let same = self
308 .db
309 .prepare(
310 "UPDATE memories SET updated_at = ?, status = 'kept'
311 WHERE scope = ? AND scope_key = ? AND (text = ? OR fingerprint = ?) RETURNING id AS value",
312 )
313 .bind(&[now.as_str().into(), a.scope.as_str().into(), key.as_str().into(), text.as_str().into(), print.as_str().into()])?
314 .first::<String>(Some("value"))
315 .await?;
316 if let Some(id) = same {
317 return Ok(match self.memory_row(&workspace, &id).await? {
318 Some(memory) => Outcome::Ok(memory),
319 None => Outcome::fail(FailureCode::NotFound, "Memory not found."),
320 });
321 }
322 let count = self
323 .db
324 .prepare("SELECT count(*) AS value FROM memories WHERE scope = ? AND scope_key = ? AND status != 'dismissed'")
325 .bind(&[a.scope.as_str().into(), key.as_str().into()])?
326 .first::<u32>(Some("value"))
327 .await?
328 .unwrap_or_default();
329 if count >= MAX_PER_SCOPE {
330 return Ok(Outcome::fail(
331 FailureCode::Conflict,
332 format!("This {} already keeps {MAX_PER_SCOPE} memories. Remove some that no longer hold first.", a.scope.as_str()),
333 ));
334 }
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.335 // Where it came from: a person, or an agent working for one.
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait336 let from = match (&repo, a.from_number) {
337 (Some(repo), Some(number)) => Some((repo, number)),
338 _ => None,
339 };
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.340 let source_kind = match a.actor.kind {
341 PrincipalKind::User => "person",
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait342 _ => "agent",
343 };
344 let id = new_id("mem", now_ms());
345 let repo_text = repo.as_ref().map(|repo| format!("{}/{}", repo.namespace, repo.name));
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.346 // Its source, as earlier memories record theirs.
347 let reference = format!("{source_kind}:{}", a.actor.username);
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait348 self.db
349 .prepare(
350 "INSERT INTO memories
351 (id, scope, scope_key, workspace, repo, text, kind, source_kind, source_run,
352 source_repo, source_number, created_by, pinned, created_at, updated_at,
353 status, fingerprint, sources, source_ref)
354 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'kept', ?, ?, ?)",
355 )
356 .bind(&[
357 id.as_str().into(),
358 a.scope.as_str().into(),
359 key.clone().into(),
360 workspace.as_str().into(),
361 // A workspace's memory belongs to no one project, though it
362 // remembers which it was learned in.
363 if a.scope == MemoryScope::Project { repo_text.clone().map_or(JsValue::NULL, JsValue::from) } else { JsValue::NULL },
364 text.into(),
365 a.kind.as_str().into(),
366 source_kind.into(),
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.367 JsValue::NULL,
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait368 repo_text.map_or(JsValue::NULL, JsValue::from),
369 from.map_or(JsValue::NULL, |(_, number)| number.into()),
370 a.actor.username.as_str().into(),
371 u32::from(a.pinned).into(),
372 now.as_str().into(),
373 now.as_str().into(),
374 print.as_str().into(),
375 serde_json::to_string(&[&reference])?.into(),
376 reference.as_str().into(),
377 ])?
378 .run()
379 .await?;
380 Ok(match self.memory_row(&workspace, &id).await? {
381 Some(memory) => Outcome::Ok(memory),
382 None => Outcome::fail(FailureCode::NotFound, "Memory not found."),
383 })
384 }
385
386 pub(crate) async fn update_memory(&self, a: UpdateMemoryArgs) -> Result<Outcome<Memory>> {
387 let workspace = a.workspace.to_lowercase();
388 let repo_id = self.memory_repo_id(&a.id).await?;
389 if !may_write(&a.actor, &workspace, repo_id.as_deref()) || a.actor.kind == PrincipalKind::Agent {
390 return Ok(Outcome::fail(FailureCode::Forbidden, CHANGE_DENIED));
391 }
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.392 if self.memory_row(&workspace, &a.id).await?.is_none() {
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait393 return Ok(Outcome::fail(FailureCode::NotFound, "Memory not found."));
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.394 }
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait395 let text = match a.text.as_deref().map(valid_text) {
396 Some(Err(message)) => return Ok(Outcome::fail(FailureCode::Invalid, message)),
397 Some(Ok(text)) => Some(text),
398 None => None,
399 };
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.400 let print = text.as_deref().map(fingerprint);
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait401 self.db
402 .prepare(
403 "UPDATE memories SET text = COALESCE(?, text), kind = COALESCE(?, kind),
404 pinned = COALESCE(?, pinned), fingerprint = COALESCE(?, fingerprint), updated_at = ?
405 WHERE id = ? AND workspace = ?",
406 )
407 .bind(&[
408 text.map_or(JsValue::NULL, JsValue::from),
409 a.kind.map_or(JsValue::NULL, |kind| kind.as_str().into()),
410 a.pinned.map_or(JsValue::NULL, |pinned| u32::from(pinned).into()),
411 print.map_or(JsValue::NULL, JsValue::from),
412 rfc3339(now_ms()).into(),
413 a.id.as_str().into(),
414 workspace.as_str().into(),
415 ])?
416 .run()
417 .await?;
418 Ok(match self.memory_row(&workspace, &a.id).await? {
419 Some(memory) => Outcome::Ok(memory),
420 None => Outcome::fail(FailureCode::NotFound, "Memory not found."),
421 })
422 }
423
424 pub(crate) async fn delete_memory(&self, a: DeleteMemoryArgs) -> Result<Outcome<bool>> {
425 let workspace = a.workspace.to_lowercase();
426 let repo_id = self.memory_repo_id(&a.id).await?;
427 if !may_write(&a.actor, &workspace, repo_id.as_deref()) || a.actor.kind == PrincipalKind::Agent {
428 return Ok(Outcome::fail(FailureCode::Forbidden, CHANGE_DENIED));
429 }
430 let gone = self
431 .db
432 .prepare("DELETE FROM memories WHERE id = ? AND workspace = ? RETURNING id AS value")
433 .bind(&[a.id.as_str().into(), workspace.as_str().into()])?
434 .first::<String>(Some("value"))
435 .await?;
436 Ok(match gone {
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.437 Some(_) => Outcome::Ok(true),
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait438 None => Outcome::fail(FailureCode::NotFound, "Memory not found."),
439 })
440 }
441
442 pub(crate) async fn recall(&self, a: RecallArgs) -> Result<Outcome<Memories>> {
443 let repo = match self.repo(&a.repo, &a.viewer).await? {
444 Outcome::Ok(repo) => repo,
445 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
446 };
447 let workspace = repo.namespace.to_lowercase();
448 // Found for the viewer, so they can read the project and its
449 // memory; the workspace's is its members'.
450 let member = a.viewer.as_ref().is_some_and(|viewer| viewer.is_member(&workspace));
451 let words: Vec<String> = a
452 .query
453 .as_deref()
454 .unwrap_or_default()
455 .split_whitespace()
456 .map(str::to_lowercase)
457 .collect();
458 let limit = a.limit.unwrap_or(DEFAULT_RECALL).clamp(1, 100) as usize;
459 let matching = |memories: Vec<Memory>| -> Vec<Memory> {
460 memories
461 .into_iter()
462 .filter(|memory| {
463 let text = memory.text.to_lowercase();
464 words.iter().all(|word| text.contains(word.as_str()))
465 })
466 .take(limit)
467 .collect()
468 };
469 let found = Memories {
470 project: matching(self.memories_of(MemoryScope::Project, &repo.id, MAX_PER_SCOPE).await?),
471 workspace: if member {
472 matching(self.memories_of(MemoryScope::Workspace, &workspace, MAX_PER_SCOPE).await?)
473 } else {
474 Vec::new()
475 },
476 };
477 let ids: Vec<String> = found
478 .project
479 .iter()
480 .chain(&found.workspace)
481 .map(|memory| memory.id.clone())
482 .collect();
483 self.mark_used(&ids).await?;
484 Ok(Outcome::Ok(found))
485 }
486}
487
488#[cfg(test)]
489mod tests {
490 use super::*;
491
492 #[test]
493 fn transfer_and_deletion_statements_take_what_they_name() {
494 for sql in TRANSFERRED.iter().chain(crate::guardrails::TRANSFERRED) {
495 let n = g1t_kit::transfer::parameters(sql);
496 assert!((1..=5).contains(&n), "{sql}");
497 }
498 for sql in DELETED {
499 assert_eq!(g1t_kit::rename::parameters(sql), 1, "{sql}");
500 }
501 }
502
503 #[test]
504 fn rename_statements_take_the_two_slugs() {
505 for sql in RENAMED {
506 assert_eq!(g1t_kit::rename::parameters(sql), 2, "{sql}");
507 }
508 }
509
510 #[test]
511 fn secrets_are_refused_with_a_way_out() {
512 // Joined at run time, so no whole key sits in the source for scanners to flag.
513 let key = ["ghp", "_", "abcdefghijklmnopqrstuvwxyz0123456789"].concat();
514 let refused = valid_text(&format!("deploy with {key}")).unwrap_err();
515 assert!(refused.contains("never holds secrets"));
516 assert_eq!(valid_text(" We use pnpm. ").unwrap(), "We use pnpm.");
517 assert!(valid_text(" ").is_err());
518 }
519
520 fn person(base: Option<access::BasePermission>, grants: &[(&str, access::RepoRole)], member: bool) -> User {
521 User {
522 id: "usr_1".into(),
523 username: "ana".into(),
524 verified: true,
525 workspaces: if member {
526 vec![g1t_contracts::Membership { base_permission: base, ..g1t_contracts::Membership::member("acme") }]
527 } else {
528 Vec::new()
529 },
530 grants: grants
531 .iter()
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar532 .map(|(id, role)| access::RepoGrant { repo_id: (*id).into(), workspace: "acme".into(), role: *role, team: None })
A last-commits walk runs to the end unless asked for a budget; the page bounds its own wait533 .collect(),
534 ..User::default()
535 }
536 }
537
538 #[test]
539 fn a_workspaces_memory_is_its_members_and_a_projects_needs_write() {
540 use access::{BasePermission, RepoRole};
541 let member = person(None, &[], true);
542 assert!(may_write(&member, "acme", None));
543 assert!(may_write(&member, "acme", Some("rep_1")));
544 // A reader may not change a project's memory, nor may an outsider.
545 let reader = person(Some(BasePermission::Read), &[], true);
546 assert!(may_write(&reader, "acme", None));
547 assert!(!may_write(&reader, "acme", Some("rep_1")));
548 let outsider = person(None, &[("rep_1", RepoRole::Write)], false);
549 assert!(may_write(&outsider, "acme", Some("rep_1")));
550 assert!(!may_write(&outsider, "acme", Some("rep_2")));
551 assert!(!may_write(&outsider, "acme", None));
552 let unverified = User { verified: false, ..person(None, &[], true) };
553 assert!(!may_write(&unverified, "acme", None));
554 }
555}

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