Feat: transcript retention (#123)

* feat(retention): prune old agent transcripts + panel-tunable setting

Agents write a {session-id}.jsonl per spawn under ~/.claude/projects; nothing
ever deleted them, so the operator's bind-mounted ~/.claude grew without bound.

Add a throttled orchestrator sweep that prunes agent-owned transcripts (the
shared -app dir + per-workspace dirs) older than a retention window — and ONLY
agent-owned dirs, never the operator's own Claude sessions (proven by the
temp-dir selection tests). The window is panel-tunable: a new system_settings
key-value table (migration 027) holds transcript_retention_days, read via
SettingsService with the roboco.config default (14d) as the fallback, exposed
through GET/PUT /api/settings. Panel wiring follows.

* feat(panel): add a panel-tunable transcript retention control

Wire the settings page to the /api/settings backend: a settings API client and
a self-contained Transcript Retention card (React Query) that loads
transcript_retention_days and saves it back, with client-side validation. The
existing settings controls are unchanged.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-06-12 23:11:01 +02:00
committed by GitHub
co-authored by Renn F
parent 718d7dd83e
commit fbbb7b3251
15 changed files with 619 additions and 0 deletions
@@ -0,0 +1,93 @@
"use client";
import { useEffect, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { settingsApi } from "@/lib/api";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { HardDrive, Save } from "lucide-react";
import { toast } from "sonner";
const RETENTION_KEY = "transcript_retention_days";
const DEFAULT_RETENTION = "14";
export function TranscriptRetentionCard() {
const queryClient = useQueryClient();
const [days, setDays] = useState<string>(DEFAULT_RETENTION);
const { data: settings, isLoading } = useQuery({
queryKey: ["settings"],
queryFn: settingsApi.getAll,
});
useEffect(() => {
const stored = settings?.[RETENTION_KEY];
if (stored !== undefined) {
setDays(stored);
}
}, [settings]);
const saveMutation = useMutation({
mutationFn: (value: string) => settingsApi.update(RETENTION_KEY, value),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["settings"] });
toast.success("Transcript retention updated");
},
onError: (error) => {
toast.error(
`Failed to save: ${error instanceof Error ? error.message : "Unknown error"}`,
);
},
});
const handleSave = () => {
const parsed = Number(days);
if (!Number.isInteger(parsed) || parsed < 1) {
toast.error("Retention must be a whole number of days (at least 1)");
return;
}
saveMutation.mutate(String(parsed));
};
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<HardDrive className="h-5 w-5" />
Transcript Retention
</CardTitle>
<CardDescription>
How long agent transcripts are kept before the background sweep prunes
them. Only agent-owned transcripts are pruned your own Claude
sessions are never touched.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="transcript-retention-days">Retention window (days)</Label>
<Input
id="transcript-retention-days"
type="number"
min={1}
value={days}
disabled={isLoading}
onChange={(e) => setDays(e.target.value)}
className="max-w-[160px]"
/>
</div>
<Button onClick={handleSave} disabled={saveMutation.isPending || isLoading}>
<Save className="h-4 w-4 mr-2" />
{saveMutation.isPending ? "Saving..." : "Save"}
</Button>
</CardContent>
</Card>
);
}