feat: delete a browser's copy of an AI session past its workspace retention (#11156)

* feat: delete a browser's copy of an AI session past its workspace retention

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: tell the AI session retention only to a member who can reach the workspace

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: keep the retention sweep's design narrative in the docs, not the code

* fix: give the session retention its own route, leaving the status contract alone

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: shorten the retention route comment to its constraints

* docs: name the two clocks in the retention setting, and the deploy window

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-09-16 09:57:43 +02:00
committed by GitHub
co-authored by Claude Opus 5
parent ee6d317e31
commit a48ae656ae
17 changed files with 718 additions and 55 deletions
@@ -227,6 +227,10 @@ pub fn global_service() -> Router {
.route("/list", get(list_workspaces))
.route("/users", get(user_workspaces))
.route("/session_workspace_status", post(session_workspace_status))
.route(
"/session_workspace_retention",
post(session_workspace_retention),
)
.route("/create", post(create_workspace))
.route("/create_fork", post(deprecated_create_workspace_fork))
.route("/exists", post(exists_workspace))
@@ -5686,6 +5690,42 @@ async fn session_workspace_status(
Ok(Json(statuses))
}
/// The AI session retention a browser deletes its local copies by (docs/ai-session-backups.md).
/// Its own route, not a field on the status above, whose shape an older tab still reads. Unlike
/// a status, it answers only for a workspace this caller can be authed into: a setting is the
/// workspace's to tell, so a disabled membership gets none though its sessions still reconcile.
async fn session_workspace_retention(
Extension(db): Extension<DB>,
authed: ApiAuthed,
Json(req): Json<SessionWorkspaceStatusRequest>,
) -> JsonResult<HashMap<String, u32>> {
if req.workspace_ids.len() > 1000 {
return Err(Error::BadRequest(
"Too many workspace ids (max 1000)".to_string(),
));
}
let email = &authed.email;
let is_superadmin = windmill_api_auth::is_super_admin_authed(&db, &authed).await?;
let rows = sqlx::query!(
"SELECT workspace_settings.workspace_id AS \"id!\",
workspace_settings.ai_config->'sessions_retention_days' AS retention
FROM workspace_settings
LEFT JOIN usr ON usr.workspace_id = workspace_settings.workspace_id AND usr.email = $2
WHERE workspace_settings.workspace_id = ANY($1)
AND ($3 OR (usr.email IS NOT NULL AND NOT usr.disabled))",
&req.workspace_ids[..],
email,
is_superadmin,
)
.fetch_all(&db)
.await?;
let days = rows
.into_iter()
.filter_map(|r| sessions_retention_days(r.retention.as_ref()).map(|days| (r.id, days)))
.collect();
Ok(Json(days))
}
/// The instance critical alert channels belong to the instance operator, who on cloud is
/// not the workspace owner and never opted into a tenant's job failures. Fork workspaces run
/// throwaway copies of their parent's runnables, so instance-wide operational alerting must