Skip to content

Commit c2373d6

Browse files
Add checkout_objective RPC
Introduced a PostgreSQL function checkout_objective to atomically lock an objective. Replaced previous JS-based checkout with RPC call to set locked_by/locked_at when eligible, enabling safe first-time checkout and stale-lock recovery. X-Lovable-Edit-ID: edt-04f69ba7-cbb8-4fd5-9a69-6fd0a72d9009 Co-authored-by: magnusfroste <38864257+magnusfroste@users.noreply.github.com>
2 parents 13b9955 + ce1af91 commit c2373d6

File tree

3 files changed

+29
-10
lines changed

3 files changed

+29
-10
lines changed

src/integrations/supabase/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2850,6 +2850,10 @@ export type Database = {
28502850
}
28512851
}
28522852
Functions: {
2853+
checkout_objective: {
2854+
Args: { p_locked_by?: string; p_objective_id: string }
2855+
Returns: boolean
2856+
}
28532857
dispatch_automation_event: {
28542858
Args: {
28552859
entity_id?: string

supabase/functions/_shared/agent-reason.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -417,17 +417,13 @@ export async function saveHeartbeatState(supabase: any, state: HeartbeatState):
417417
// ─── Atomic Task Checkout ─────────────────────────────────────────────────────
418418

419419
export async function checkoutObjective(supabase: any, objectiveId: string): Promise<boolean> {
420-
// Atomic: only succeeds if not locked or lock is stale (>30 min)
421-
const staleThreshold = new Date(Date.now() - 30 * 60_000).toISOString();
422-
const { data, error } = await supabase
423-
.from('agent_objectives')
424-
.update({ locked_by: 'heartbeat', locked_at: new Date().toISOString() })
425-
.eq('id', objectiveId)
426-
.or(`locked_by.is.null,locked_at.lt.${staleThreshold}`)
427-
.select('id')
428-
.maybeSingle();
420+
// Atomic checkout via database function — prevents race conditions
421+
const { data, error } = await supabase.rpc('checkout_objective', {
422+
p_objective_id: objectiveId,
423+
p_locked_by: 'heartbeat',
424+
});
429425

430-
return !error && !!data;
426+
return !error && data === true;
431427
}
432428

433429
export async function releaseObjective(supabase: any, objectiveId: string): Promise<void> {
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
2+
CREATE OR REPLACE FUNCTION public.checkout_objective(p_objective_id uuid, p_locked_by text DEFAULT 'heartbeat')
3+
RETURNS boolean
4+
LANGUAGE plpgsql
5+
SECURITY DEFINER
6+
SET search_path TO 'public'
7+
AS $$
8+
DECLARE
9+
rows_affected integer;
10+
BEGIN
11+
UPDATE agent_objectives
12+
SET locked_by = p_locked_by, locked_at = now()
13+
WHERE id = p_objective_id
14+
AND (locked_by IS NULL OR locked_at < now() - interval '30 minutes');
15+
16+
GET DIAGNOSTICS rows_affected = ROW_COUNT;
17+
RETURN rows_affected > 0;
18+
END;
19+
$$;

0 commit comments

Comments
 (0)