@rulemetric/local 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/meta.json +2 -2
- package/dist/server.mjs +498 -297
- package/dist/supabase/migrations/00218_backfill_session_events_model.sql +30 -0
- package/dist/supabase/migrations/00219_weekly_digest.sql +64 -0
- package/dist/supabase/migrations/00220_snapshot_payload_lifecycle.sql +107 -0
- package/dist/supabase/migrations/00221_snapshot_payload_expiry.sql +34 -0
- package/dist/supabase/migrations/00222_memory_experiment_engine.sql +20 -0
- package/dist/supabase/migrations/00223_measurement_preferences.sql +21 -0
- package/dist/supabase/migrations/00224_measurement_job_snapshots.sql +246 -0
- package/dist/web/assets/{docs-CP7YCEUM.js → docs-sROR5bQ8.js} +125 -115
- package/dist/web/assets/index-Cpw-uDNX.css +1 -0
- package/dist/web/assets/{index-CujhsOsn.js → index-D7l_eoTZ.js} +50 -50
- package/dist/web/index.html +3 -3
- package/package.json +2 -2
- package/dist/web/assets/index-YQbc4i81.css +0 -1
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
-- 00218_backfill_session_events_model.sql — populate session_events.model from
|
|
2
|
+
-- the metadata copy the Claude Code adapter has always written.
|
|
3
|
+
--
|
|
4
|
+
-- Measured on prod 2026-09-03 (read-only): 71,477 token-bearing session_events
|
|
5
|
+
-- rows had model IS NULL, and every one of them carried the id in
|
|
6
|
+
-- metadata->>'model'. The claude_code adapter (packages/session) stamped
|
|
7
|
+
-- metadata.model but never the top-level event field, so /import and reimport
|
|
8
|
+
-- wrote NULL into the column. The insights cost rollup
|
|
9
|
+
-- (apps/api/src/routes/insights/compute.ts) keys pricing on that column and
|
|
10
|
+
-- fell back to the constant claude-sonnet-4-6 for every hooks-first session —
|
|
11
|
+
-- /usage reported a cost that was not the user's spend. GET /api/pricing/coverage
|
|
12
|
+
-- (the weekly cron_refresh_pricing worklist) reads the same column, so those
|
|
13
|
+
-- models were never even queued for a price row.
|
|
14
|
+
--
|
|
15
|
+
-- Keep-current half: the adapter now sets event.model and the ingest mapping
|
|
16
|
+
-- writes it (same PR). This is the one-time backfill. Idempotent (touches only
|
|
17
|
+
-- model IS NULL); placeholders ('', 'unknown', '<synthetic>') are skipped so
|
|
18
|
+
-- they never become a cost bucket — the same set resolveModelId() rejects.
|
|
19
|
+
--
|
|
20
|
+
-- The deploy runner applies each file inside ONE transaction (psql -1). A DO
|
|
21
|
+
-- block that loops in batches would not dodge statement_timeout — the DO block
|
|
22
|
+
-- is itself one statement — so the ceiling is raised for this transaction
|
|
23
|
+
-- instead, as 00095 did. ~71k rows behind a seq scan of session_events.
|
|
24
|
+
SET LOCAL statement_timeout = '600s';
|
|
25
|
+
|
|
26
|
+
UPDATE session_events
|
|
27
|
+
SET model = metadata->>'model'
|
|
28
|
+
WHERE model IS NULL
|
|
29
|
+
AND metadata->>'model' IS NOT NULL
|
|
30
|
+
AND metadata->>'model' NOT IN ('', 'unknown', '<synthetic>');
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
-- 00219: the weekly digest email — one mail per active user per week.
|
|
2
|
+
--
|
|
3
|
+
-- Our first external user ran the product for five weeks and last opened the
|
|
4
|
+
-- web app on day one. In that time it analysed 107 of his sessions, adopted 26
|
|
5
|
+
-- blocks into his CLAUDE.md and started 9 randomised experiments on his
|
|
6
|
+
-- projects, and nothing told him. The digest is the answer to "what should I
|
|
7
|
+
-- be looking at week to week": numbers the server already has, plus an honest
|
|
8
|
+
-- date for when each running experiment can reach a verdict. It never claims
|
|
9
|
+
-- money saved or effectiveness gained (docs/architecture/effectiveness.md §12).
|
|
10
|
+
--
|
|
11
|
+
-- Three pieces:
|
|
12
|
+
-- notification_preferences.weekly_digest — the opt-out. Default true: the
|
|
13
|
+
-- digest is the product telling the
|
|
14
|
+
-- user what it did on their behalf.
|
|
15
|
+
-- `unsubscribed_at` (the one-click
|
|
16
|
+
-- unsubscribe) also stops it.
|
|
17
|
+
-- weekly_digest_deliveries — one row per (user, week). The
|
|
18
|
+
-- sender reserves the row BEFORE
|
|
19
|
+
-- calling the provider, so a crash
|
|
20
|
+
-- between send and record cannot
|
|
21
|
+
-- double-mail on retry.
|
|
22
|
+
-- weekly_digest_runs — one row per week. The in-process
|
|
23
|
+
-- scheduler CLAIMS the week with an
|
|
24
|
+
-- INSERT ... ON CONFLICT DO NOTHING,
|
|
25
|
+
-- so two API instances (or a
|
|
26
|
+
-- restart mid-run) cannot both send.
|
|
27
|
+
|
|
28
|
+
alter table notification_preferences
|
|
29
|
+
add column if not exists weekly_digest boolean not null default true;
|
|
30
|
+
|
|
31
|
+
create table if not exists weekly_digest_deliveries (
|
|
32
|
+
user_id uuid not null references profiles(id) on delete cascade,
|
|
33
|
+
week_start date not null,
|
|
34
|
+
sent_at timestamptz not null default now(),
|
|
35
|
+
provider_message_id text,
|
|
36
|
+
primary key (user_id, week_start)
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
create table if not exists weekly_digest_runs (
|
|
40
|
+
week_start date primary key,
|
|
41
|
+
started_at timestamptz not null default now(),
|
|
42
|
+
finished_at timestamptz,
|
|
43
|
+
sent int,
|
|
44
|
+
skipped int,
|
|
45
|
+
failed int
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
-- RLS: the API reaches these through the service connection (bypasses RLS);
|
|
49
|
+
-- through PostgREST a user may read their own delivery rows and a superadmin
|
|
50
|
+
-- may read everything. Nothing writes through PostgREST.
|
|
51
|
+
alter table weekly_digest_deliveries enable row level security;
|
|
52
|
+
alter table weekly_digest_runs enable row level security;
|
|
53
|
+
|
|
54
|
+
drop policy if exists weekly_digest_deliveries_own_read on weekly_digest_deliveries;
|
|
55
|
+
create policy weekly_digest_deliveries_own_read on weekly_digest_deliveries for select
|
|
56
|
+
using (user_id = auth.uid());
|
|
57
|
+
|
|
58
|
+
drop policy if exists weekly_digest_deliveries_superadmin_read on weekly_digest_deliveries;
|
|
59
|
+
create policy weekly_digest_deliveries_superadmin_read on weekly_digest_deliveries for select
|
|
60
|
+
using (exists (select 1 from profiles where id = auth.uid() and is_superadmin = true));
|
|
61
|
+
|
|
62
|
+
drop policy if exists weekly_digest_runs_superadmin_read on weekly_digest_runs;
|
|
63
|
+
create policy weekly_digest_runs_superadmin_read on weekly_digest_runs for select
|
|
64
|
+
using (exists (select 1 from profiles where id = auth.uid() and is_superadmin = true));
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
-- Snapshot payload lifecycle: compact capture heads, durable Storage intents,
|
|
2
|
+
-- and explicit expiry state. These DDL statements are additive and idempotent
|
|
3
|
+
-- so a local replay and an additive production rollout share the same schema.
|
|
4
|
+
|
|
5
|
+
create table if not exists session_capture_heads (
|
|
6
|
+
session_id uuid primary key references sessions(id) on delete cascade,
|
|
7
|
+
sequence integer not null,
|
|
8
|
+
raw_message_hashes jsonb not null,
|
|
9
|
+
version integer not null,
|
|
10
|
+
updated_at timestamptz not null default now()
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
create table if not exists snapshot_delta_objects (
|
|
14
|
+
ref text primary key,
|
|
15
|
+
-- NULL deliberately permits a catalog record for a shared legacy ref.
|
|
16
|
+
snapshot_id uuid,
|
|
17
|
+
compressed_bytes integer not null,
|
|
18
|
+
state text not null,
|
|
19
|
+
created_at timestamptz not null default now(),
|
|
20
|
+
last_attempt_at timestamptz,
|
|
21
|
+
attempts integer not null default 0,
|
|
22
|
+
last_error text
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
alter table context_snapshots
|
|
26
|
+
add column if not exists messages_base_length integer,
|
|
27
|
+
add column if not exists payload_pruned_at timestamptz,
|
|
28
|
+
add column if not exists delta_offload_state text not null default 'inline',
|
|
29
|
+
add column if not exists delta_gzip_bytes integer,
|
|
30
|
+
add column if not exists delta_offload_error text,
|
|
31
|
+
add column if not exists delta_offload_attempts integer not null default 0;
|
|
32
|
+
|
|
33
|
+
alter table content_blobs
|
|
34
|
+
add column if not exists last_referenced_at timestamptz not null default now();
|
|
35
|
+
|
|
36
|
+
do $$
|
|
37
|
+
begin
|
|
38
|
+
if not exists (
|
|
39
|
+
select 1 from pg_constraint where conname = 'context_snapshots_delta_offload_state_check'
|
|
40
|
+
) then
|
|
41
|
+
alter table context_snapshots add constraint context_snapshots_delta_offload_state_check
|
|
42
|
+
check (delta_offload_state in ('inline', 'pending', 'offloaded', 'expired'));
|
|
43
|
+
end if;
|
|
44
|
+
if not exists (
|
|
45
|
+
select 1 from pg_constraint where conname = 'snapshot_delta_objects_state_check'
|
|
46
|
+
) then
|
|
47
|
+
alter table snapshot_delta_objects add constraint snapshot_delta_objects_state_check
|
|
48
|
+
check (state in ('uploading', 'ready', 'delete_pending', 'failed'));
|
|
49
|
+
end if;
|
|
50
|
+
end $$;
|
|
51
|
+
|
|
52
|
+
update context_snapshots
|
|
53
|
+
set delta_offload_state = case
|
|
54
|
+
when payload_pruned_at is not null
|
|
55
|
+
or (messages_delta is null and messages_delta_ref is null) then 'expired'
|
|
56
|
+
when messages_delta_ref is not null then 'offloaded'
|
|
57
|
+
else 'inline'
|
|
58
|
+
end;
|
|
59
|
+
|
|
60
|
+
-- Production: build these outside a migration transaction with CONCURRENTLY.
|
|
61
|
+
-- Local migration replay creates their equivalent indexes synchronously.
|
|
62
|
+
create index if not exists idx_snapshots_unpruned_captured
|
|
63
|
+
on context_snapshots (captured_at, id)
|
|
64
|
+
where payload_pruned_at is null;
|
|
65
|
+
create index if not exists idx_snapshots_pending_offload
|
|
66
|
+
on context_snapshots (delta_offload_state, captured_at, id)
|
|
67
|
+
where delta_offload_state = 'pending';
|
|
68
|
+
create index if not exists idx_snapshots_delta_ref
|
|
69
|
+
on context_snapshots (messages_delta_ref)
|
|
70
|
+
where messages_delta_ref is not null;
|
|
71
|
+
create index if not exists idx_snapshot_delta_objects_state_created
|
|
72
|
+
on snapshot_delta_objects (state, created_at);
|
|
73
|
+
create unique index if not exists snapshot_delta_objects_snapshot_id_unique
|
|
74
|
+
on snapshot_delta_objects (snapshot_id)
|
|
75
|
+
where snapshot_id is not null;
|
|
76
|
+
|
|
77
|
+
create or replace function public.cleanup_stale_session_capture_heads()
|
|
78
|
+
returns bigint
|
|
79
|
+
language plpgsql
|
|
80
|
+
security definer
|
|
81
|
+
set search_path = public
|
|
82
|
+
as $function$
|
|
83
|
+
declare
|
|
84
|
+
removed bigint;
|
|
85
|
+
begin
|
|
86
|
+
delete from session_capture_heads head
|
|
87
|
+
using sessions session
|
|
88
|
+
where head.session_id = session.id
|
|
89
|
+
and head.updated_at < now() - interval '14 days'
|
|
90
|
+
and (session.ended_at is not null or session.started_at < now() - interval '14 days');
|
|
91
|
+
get diagnostics removed = row_count;
|
|
92
|
+
return removed;
|
|
93
|
+
end;
|
|
94
|
+
$function$;
|
|
95
|
+
|
|
96
|
+
do $$
|
|
97
|
+
begin
|
|
98
|
+
if exists (select 1 from cron.job where jobname = 'cleanup-stale-session-capture-heads') then
|
|
99
|
+
perform cron.unschedule('cleanup-stale-session-capture-heads');
|
|
100
|
+
end if;
|
|
101
|
+
end $$;
|
|
102
|
+
|
|
103
|
+
select cron.schedule(
|
|
104
|
+
'cleanup-stale-session-capture-heads',
|
|
105
|
+
'30 4 * * *',
|
|
106
|
+
$$select public.cleanup_stale_session_capture_heads()$$
|
|
107
|
+
);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
-- Expiry is a marker-first operation: readers stop resolving raw payloads as
|
|
2
|
+
-- soon as this runs, while the Node collector retains the object reference
|
|
3
|
+
-- until Storage confirms deletion.
|
|
4
|
+
create or replace function public.prune_old_snapshot_content(p_batch_size integer default 1000)
|
|
5
|
+
returns text
|
|
6
|
+
language plpgsql
|
|
7
|
+
security definer
|
|
8
|
+
set search_path = public
|
|
9
|
+
as $function$
|
|
10
|
+
declare
|
|
11
|
+
v_count bigint;
|
|
12
|
+
begin
|
|
13
|
+
with candidates as (
|
|
14
|
+
select id
|
|
15
|
+
from context_snapshots
|
|
16
|
+
where payload_pruned_at is null
|
|
17
|
+
and captured_at < now() - interval '90 days'
|
|
18
|
+
order by captured_at, id
|
|
19
|
+
limit p_batch_size
|
|
20
|
+
)
|
|
21
|
+
update context_snapshots snapshot
|
|
22
|
+
set payload_pruned_at = now(),
|
|
23
|
+
delta_offload_state = 'expired',
|
|
24
|
+
messages_delta = null,
|
|
25
|
+
messages_summary = '[]'::jsonb,
|
|
26
|
+
system_prompt = '',
|
|
27
|
+
tools = '[]'::jsonb,
|
|
28
|
+
injected_instructions = '[]'::jsonb
|
|
29
|
+
from candidates
|
|
30
|
+
where snapshot.id = candidates.id;
|
|
31
|
+
get diagnostics v_count = row_count;
|
|
32
|
+
return format('marked %s snapshot payloads expired', v_count);
|
|
33
|
+
end;
|
|
34
|
+
$function$;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
-- Pin controlled memory experiments to one harness. Without this field, every
|
|
2
|
+
-- row implicitly ran Claude and there was no supported way to ask whether the
|
|
3
|
+
-- same memory format improves outcomes within Codex or Pi.
|
|
4
|
+
alter table public.memory_experiments
|
|
5
|
+
add column if not exists engine text not null default 'claude';
|
|
6
|
+
|
|
7
|
+
do $block$
|
|
8
|
+
begin
|
|
9
|
+
if not exists (
|
|
10
|
+
select 1
|
|
11
|
+
from pg_constraint
|
|
12
|
+
where conname = 'memory_experiments_engine_check'
|
|
13
|
+
and conrelid = 'public.memory_experiments'::regclass
|
|
14
|
+
) then
|
|
15
|
+
alter table public.memory_experiments
|
|
16
|
+
add constraint memory_experiments_engine_check
|
|
17
|
+
check (engine in ('claude', 'codex', 'pi'));
|
|
18
|
+
end if;
|
|
19
|
+
end
|
|
20
|
+
$block$;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
ALTER TABLE public.profiles ADD COLUMN measurement_preferences jsonb;
|
|
2
|
+
ALTER TABLE public.profiles ADD CONSTRAINT profiles_measurement_preferences_check CHECK (
|
|
3
|
+
measurement_preferences IS NULL OR (
|
|
4
|
+
jsonb_typeof(measurement_preferences) = 'object'
|
|
5
|
+
AND measurement_preferences ?& ARRAY['engine', 'model']
|
|
6
|
+
AND (measurement_preferences - 'engine' - 'model') = '{}'::jsonb
|
|
7
|
+
AND (
|
|
8
|
+
(measurement_preferences->'engine' = 'null'::jsonb AND measurement_preferences->'model' = 'null'::jsonb)
|
|
9
|
+
OR (
|
|
10
|
+
measurement_preferences->>'engine' IN ('claude', 'codex')
|
|
11
|
+
AND (
|
|
12
|
+
(measurement_preferences->>'engine' = 'claude' AND measurement_preferences->'model' = 'null'::jsonb)
|
|
13
|
+
OR (jsonb_typeof(measurement_preferences->'model') = 'string'
|
|
14
|
+
AND length(btrim(measurement_preferences->>'model')) BETWEEN 1 AND 200)
|
|
15
|
+
)
|
|
16
|
+
)
|
|
17
|
+
)
|
|
18
|
+
) IS TRUE
|
|
19
|
+
);
|
|
20
|
+
COMMENT ON COLUMN public.profiles.measurement_preferences IS
|
|
21
|
+
'Explicit engine/model for future measurement jobs. NULL retains legacy behavior. Does not grant scheduling, spending, or rewrite consent.';
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
-- Immutable selection for NEW jobs only. Existing payloads and all producer
|
|
2
|
+
-- eligibility predicates from 00210 remain unchanged.
|
|
3
|
+
create or replace function public.measurement_execution_for_user(owner_id uuid, workload text)
|
|
4
|
+
returns jsonb
|
|
5
|
+
language plpgsql stable
|
|
6
|
+
set search_path = public
|
|
7
|
+
as $$
|
|
8
|
+
declare preference jsonb;
|
|
9
|
+
begin
|
|
10
|
+
if workload not in ('eval', 'harbor') then
|
|
11
|
+
raise exception 'Unknown measurement workload: %', workload;
|
|
12
|
+
end if;
|
|
13
|
+
select measurement_preferences into preference from public.profiles where id = owner_id;
|
|
14
|
+
if preference is null or preference->>'engine' is null then return null; end if;
|
|
15
|
+
return jsonb_build_object(
|
|
16
|
+
'version', 1,
|
|
17
|
+
'engine', preference->>'engine',
|
|
18
|
+
'model', coalesce(nullif(btrim(preference->>'model'), ''),
|
|
19
|
+
case when workload = 'harbor' then 'claude-haiku-4-5' else null end),
|
|
20
|
+
'source', 'account'
|
|
21
|
+
);
|
|
22
|
+
end;
|
|
23
|
+
$$;
|
|
24
|
+
revoke all on function public.measurement_execution_for_user(uuid, text) from public, anon, authenticated;
|
|
25
|
+
grant execute on function public.measurement_execution_for_user(uuid, text) to service_role;
|
|
26
|
+
|
|
27
|
+
-- Keep a canonical model attached to the engine that supplied its evidence.
|
|
28
|
+
-- Explicit historical model-only pins still mean Claude; inferred GPT judges
|
|
29
|
+
-- must not acquire that legacy default on their second scheduled batch.
|
|
30
|
+
create or replace function public.eval_target_judge_engine(p_target_id uuid)
|
|
31
|
+
returns text
|
|
32
|
+
language sql stable security definer
|
|
33
|
+
set search_path = public
|
|
34
|
+
as $$
|
|
35
|
+
with target as (
|
|
36
|
+
select nullif(btrim(metadata->>'graderModel'), '') as pin,
|
|
37
|
+
coalesce(metadata->>'graderEngine', 'claude') as pin_engine,
|
|
38
|
+
public.eval_target_judge_segment(id) as elected_model
|
|
39
|
+
from eval_targets where id = p_target_id
|
|
40
|
+
), elector as (
|
|
41
|
+
select r.grading->>'judge_model' as model,
|
|
42
|
+
coalesce(r.grading->>'judge_engine', 'claude') as engine, r.created_at,
|
|
43
|
+
(r.grading->>'judge_model_requested' is null
|
|
44
|
+
or r.grading->>'judge_model_requested' = r.grading->>'judge_model') as clean,
|
|
45
|
+
case when jsonb_typeof(r.grading->'expectations') = 'array' then (
|
|
46
|
+
select count(*) from jsonb_array_elements(r.grading->'expectations') e
|
|
47
|
+
where coalesce(e->>'method', 'llm') = 'llm'
|
|
48
|
+
and coalesce(e->>'errored', 'false') <> 'true'
|
|
49
|
+
) else 0 end as llm_total,
|
|
50
|
+
(select count(*) from eval_grade_annotations a
|
|
51
|
+
where a.eval_target_id = p_target_id and a.eval_run_id = r.id
|
|
52
|
+
and a.judge_method = 'llm') as llm_annotated
|
|
53
|
+
from eval_runs r
|
|
54
|
+
where r.eval_target_id = p_target_id and r.status = 'completed'
|
|
55
|
+
and r.configuration in ('with_target', 'without_target')
|
|
56
|
+
and r.grading->>'judge_model' is not null
|
|
57
|
+
), eligible as (
|
|
58
|
+
select e.* from elector e
|
|
59
|
+
where (e.clean or not exists (select 1 from elector where clean))
|
|
60
|
+
and e.model = (select elected_model from target)
|
|
61
|
+
)
|
|
62
|
+
select case when t.pin is not null then t.pin_engine else (
|
|
63
|
+
select engine from eligible group by engine
|
|
64
|
+
order by count(*) filter (where llm_total > 0 and llm_annotated >= llm_total) desc,
|
|
65
|
+
max(created_at) desc, engine desc limit 1
|
|
66
|
+
) end from target t;
|
|
67
|
+
$$;
|
|
68
|
+
revoke all on function public.eval_target_judge_engine(uuid) from public, anon, authenticated;
|
|
69
|
+
grant execute on function public.eval_target_judge_engine(uuid) to service_role;
|
|
70
|
+
|
|
71
|
+
-- ── enqueue_instruction_evolution, health gates now read from the view ───────
|
|
72
|
+
create or replace function public.enqueue_instruction_evolution()
|
|
73
|
+
returns integer
|
|
74
|
+
language plpgsql
|
|
75
|
+
security definer
|
|
76
|
+
set search_path = public
|
|
77
|
+
as $$
|
|
78
|
+
declare
|
|
79
|
+
queued integer;
|
|
80
|
+
begin
|
|
81
|
+
with health_ok as (
|
|
82
|
+
select t.id, t.user_id
|
|
83
|
+
from eval_targets t
|
|
84
|
+
where coalesce(t.metadata->>'autoEvolvePaused', 'false') <> 'true'
|
|
85
|
+
-- Health 2a/2b/2c: ONE definition (00210), shared with eval autorun.
|
|
86
|
+
and exists (
|
|
87
|
+
select 1 from public.healthy_eval_targets h where h.eval_target_id = t.id
|
|
88
|
+
)
|
|
89
|
+
-- Live cases to measure against — at least MIN_LIVE_CASES (3).
|
|
90
|
+
-- `exists` (>= 1) was the bug: see 00183's header.
|
|
91
|
+
and (
|
|
92
|
+
select count(*) from evals ev
|
|
93
|
+
where ev.eval_target_id = t.id and ev.retired_at is null
|
|
94
|
+
) >= 3
|
|
95
|
+
-- Change budget (cheap exclusion; the API is the authority).
|
|
96
|
+
and (
|
|
97
|
+
select count(*)
|
|
98
|
+
from instruction_promotions p
|
|
99
|
+
where p.eval_target_id = t.id
|
|
100
|
+
and p.applied
|
|
101
|
+
and p.created_at > now() - interval '7 days'
|
|
102
|
+
) < 3
|
|
103
|
+
-- One in flight at a time.
|
|
104
|
+
and not exists (
|
|
105
|
+
select 1 from agent_jobs j
|
|
106
|
+
where j.task_kind = 'cron_instruction_evolution'
|
|
107
|
+
and j.status in ('pending', 'claimed', 'running')
|
|
108
|
+
and j.payload->>'evalTargetId' = t.id::text
|
|
109
|
+
)
|
|
110
|
+
),
|
|
111
|
+
manual as (
|
|
112
|
+
-- The hand-picked path, unchanged and uncapped.
|
|
113
|
+
select h.id, h.user_id
|
|
114
|
+
from health_ok h
|
|
115
|
+
join eval_targets t on t.id = h.id
|
|
116
|
+
where coalesce(t.metadata->>'autoEvolveEnabled', 'false') = 'true'
|
|
117
|
+
),
|
|
118
|
+
auto_ranked as (
|
|
119
|
+
-- The stated rule. Round-robin: never-decided first (nulls first), then
|
|
120
|
+
-- least-recently-decided, so the cap rotates through the candidate pool
|
|
121
|
+
-- instead of re-measuring the same winners nightly.
|
|
122
|
+
select h.id, h.user_id,
|
|
123
|
+
row_number() over (
|
|
124
|
+
partition by h.user_id
|
|
125
|
+
order by (
|
|
126
|
+
select max(p.created_at)
|
|
127
|
+
from instruction_promotions p
|
|
128
|
+
where p.eval_target_id = h.id
|
|
129
|
+
) asc nulls first,
|
|
130
|
+
h.id
|
|
131
|
+
) as rn
|
|
132
|
+
from health_ok h
|
|
133
|
+
join eval_targets t on t.id = h.id
|
|
134
|
+
where t.type = 'instruction'
|
|
135
|
+
and coalesce(t.metadata->>'autoEvolveOptOut', 'false') <> 'true'
|
|
136
|
+
-- Not already covered by the manual path.
|
|
137
|
+
and coalesce(t.metadata->>'autoEvolveEnabled', 'false') <> 'true'
|
|
138
|
+
-- ACTIVE (00145's load-bearing gate): a run in the last 30 days.
|
|
139
|
+
and exists (
|
|
140
|
+
select 1 from eval_runs r
|
|
141
|
+
where r.eval_target_id = t.id
|
|
142
|
+
and r.created_at > now() - interval '30 days'
|
|
143
|
+
)
|
|
144
|
+
),
|
|
145
|
+
eligible as (
|
|
146
|
+
select id, user_id from manual
|
|
147
|
+
union
|
|
148
|
+
select id, user_id from auto_ranked where rn <= 3
|
|
149
|
+
),
|
|
150
|
+
ins as (
|
|
151
|
+
insert into agent_jobs (user_id, task_kind, status, payload, dedupe_key)
|
|
152
|
+
select e.user_id,
|
|
153
|
+
'cron_instruction_evolution',
|
|
154
|
+
'pending',
|
|
155
|
+
jsonb_build_object('evalTargetId', e.id::text)
|
|
156
|
+
|| case
|
|
157
|
+
when public.eval_target_judge_segment(e.id) is not null
|
|
158
|
+
then jsonb_build_object(
|
|
159
|
+
'graderModel', public.eval_target_judge_segment(e.id),
|
|
160
|
+
'graderEngine', public.eval_target_judge_engine(e.id))
|
|
161
|
+
else '{}'::jsonb
|
|
162
|
+
end
|
|
163
|
+
|| case when public.measurement_execution_for_user(e.user_id, 'eval') is null
|
|
164
|
+
then '{}'::jsonb
|
|
165
|
+
else jsonb_build_object('measurement', public.measurement_execution_for_user(e.user_id, 'eval')) end,
|
|
166
|
+
'instruction-evolution:' || e.id::text
|
|
167
|
+
from eligible e
|
|
168
|
+
returning 1
|
|
169
|
+
)
|
|
170
|
+
select count(*)::int into queued from ins;
|
|
171
|
+
|
|
172
|
+
return queued;
|
|
173
|
+
end;
|
|
174
|
+
$$;
|
|
175
|
+
|
|
176
|
+
comment on function public.enqueue_instruction_evolution() is
|
|
177
|
+
'Daily producer for cron_instruction_evolution. Health gates 2a/2b/2c come from the healthy_eval_targets view (00210); pause, live-cases floor (>=3, 00183), change budget, round-robin enrolment and in-flight dedupe stay here. Judge segment from eval_target_judge_segment() (00193).';
|
|
178
|
+
|
|
179
|
+
-- ── enqueue_eval_autorun, health gates now read from the view ────────────────
|
|
180
|
+
create or replace function public.enqueue_eval_autorun()
|
|
181
|
+
returns integer
|
|
182
|
+
language plpgsql
|
|
183
|
+
security definer
|
|
184
|
+
set search_path = public
|
|
185
|
+
as $$
|
|
186
|
+
declare
|
|
187
|
+
queued integer;
|
|
188
|
+
begin
|
|
189
|
+
with eligible as (
|
|
190
|
+
select t.id, t.user_id,
|
|
191
|
+
-- Canonical segment (00193). Was an inline "most recent stamped run"
|
|
192
|
+
-- subquery, which is how a credit-exhaustion fallback became the pin.
|
|
193
|
+
public.eval_target_judge_segment(t.id) as current_judge,
|
|
194
|
+
public.eval_target_judge_engine(t.id) as current_judge_engine
|
|
195
|
+
from eval_targets t
|
|
196
|
+
where coalesce(t.metadata->>'autoRunEnabled', 'false') = 'true'
|
|
197
|
+
-- Gate 0 (00190): the target must be anchored to something a promotion
|
|
198
|
+
-- could land on. Without this, a batch is two LLM executions plus a
|
|
199
|
+
-- judged grade spent to rewrite a row nothing reads.
|
|
200
|
+
and (
|
|
201
|
+
t.instruction_id is not null
|
|
202
|
+
or t.project_path is not null
|
|
203
|
+
or t.name like '/%'
|
|
204
|
+
)
|
|
205
|
+
-- Gates 2a/2b/2c: ONE definition (00210), shared with evolution.
|
|
206
|
+
and exists (
|
|
207
|
+
select 1 from public.healthy_eval_targets h where h.eval_target_id = t.id
|
|
208
|
+
)
|
|
209
|
+
-- Gate 3: the target has live cases to actually run.
|
|
210
|
+
and exists (
|
|
211
|
+
select 1 from evals ev
|
|
212
|
+
where ev.eval_target_id = t.id and ev.retired_at is null
|
|
213
|
+
)
|
|
214
|
+
-- One in flight at a time, per the 00083/00086 pattern.
|
|
215
|
+
and not exists (
|
|
216
|
+
select 1 from agent_jobs j
|
|
217
|
+
where j.task_kind = 'cron_eval_autorun'
|
|
218
|
+
and j.status in ('pending', 'claimed', 'running')
|
|
219
|
+
and j.payload->>'evalTargetId' = t.id::text
|
|
220
|
+
)
|
|
221
|
+
), ins as (
|
|
222
|
+
insert into agent_jobs (user_id, task_kind, status, payload, dedupe_key)
|
|
223
|
+
select e.user_id,
|
|
224
|
+
'cron_eval_autorun',
|
|
225
|
+
'pending',
|
|
226
|
+
jsonb_build_object('evalTargetId', e.id::text, 'maxRuns', 12)
|
|
227
|
+
|| case
|
|
228
|
+
when e.current_judge is not null
|
|
229
|
+
then jsonb_build_object('graderModel', e.current_judge, 'graderEngine', e.current_judge_engine)
|
|
230
|
+
else '{}'::jsonb
|
|
231
|
+
end
|
|
232
|
+
|| case when public.measurement_execution_for_user(e.user_id, 'eval') is null
|
|
233
|
+
then '{}'::jsonb
|
|
234
|
+
else jsonb_build_object('measurement', public.measurement_execution_for_user(e.user_id, 'eval')) end,
|
|
235
|
+
'eval-autorun:' || e.id::text
|
|
236
|
+
from eligible e
|
|
237
|
+
returning 1
|
|
238
|
+
)
|
|
239
|
+
select count(*)::int into queued from ins;
|
|
240
|
+
|
|
241
|
+
return queued;
|
|
242
|
+
end;
|
|
243
|
+
$$;
|
|
244
|
+
|
|
245
|
+
comment on function public.enqueue_eval_autorun() is
|
|
246
|
+
'Daily producer for cron_eval_autorun. Health gates 2a/2b/2c come from the healthy_eval_targets view (00210); the anchor gate (00190), live-cases exists, judge segment (00193) and in-flight dedupe stay here.';
|