@rulemetric/local 0.10.0 → 0.12.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 +682 -496
- package/dist/supabase/migrations/00087_cleanup_test_data_fix.sql +1 -1
- package/dist/supabase/migrations/00184_auto_accept_budget_settings.sql +113 -0
- package/dist/supabase/migrations/00185_trial_throughput_settings.sql +43 -0
- package/dist/supabase/migrations/00186_canonical_checkout_prefers_live.sql +256 -0
- package/dist/supabase/migrations/00187_recommendation_applications.sql +32 -0
- package/dist/supabase/migrations/00188_training_producer_canonical_path.sql +117 -0
- package/dist/supabase/migrations/00189_limit_obs_window_index.sql +37 -0
- package/dist/supabase/migrations/00190_eval_targets_artifact_link.sql +176 -0
- package/dist/supabase/migrations/00191_recommendation_applications_effect_ledger.sql +106 -0
- package/dist/supabase/migrations/00192_cleanup_reaps_memberless_orgs.sql +142 -0
- package/dist/supabase/migrations/00193_judge_segment_pin_and_anchors.sql +367 -0
- package/dist/supabase/migrations/00194_refine_cases_producer.sql +91 -0
- package/dist/supabase/migrations/00195_skill_usage_views.sql +109 -0
- package/dist/supabase/migrations/00196_retire_unused_skills_producer.sql +102 -0
- package/dist/supabase/migrations/00197_auto_accept_producer_admits_skills.sql +115 -0
- package/dist/supabase/migrations/00198_unstale_non_catalog_suggestions.sql +51 -0
- package/dist/supabase/migrations/00199_auto_accept_runs_every_two_hours.sql +54 -0
- package/dist/supabase/migrations/00200_auto_accept_runs_hourly.sql +42 -0
- package/dist/web/assets/{docs-D_fJ3Svo.js → docs-KIeJWTb-.js} +35 -48
- package/dist/web/assets/index-CFJatBZ3.css +1 -0
- package/dist/web/assets/{index-D8PcENaI.js → index-MYof2fnF.js} +59 -59
- package/dist/web/index.html +3 -3
- package/package.json +2 -2
- package/dist/web/assets/index-B2QaicI_.css +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
-- Fix cleanup_stale_test_users: it failed nightly 2026-06-07..09 on
|
|
2
2
|
-- projects_created_by_fkey because projects / project_instructions gained
|
|
3
3
|
-- NO ACTION FKs to auth.users after 00062 was written. Also extend the
|
|
4
|
-
-- sweep to delete leaked smoke-test sessions — scripts/smoke-test.sh's
|
|
4
|
+
-- sweep to delete leaked smoke-test sessions — scripts/verify/smoke-test.sh's
|
|
5
5
|
-- hook-execution section creates sessions under the dev user
|
|
6
6
|
-- (external_session_id '00000000-0000-4000-8000-…') that the teardown
|
|
7
7
|
-- only closed, never deleted.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
-- 00184 — the auto-accept budget becomes a setting, and the producer stops
|
|
2
|
+
-- starving the source it was built to serve.
|
|
3
|
+
--
|
|
4
|
+
-- Three facts measured on 2026-08-12, all from the same project:
|
|
5
|
+
--
|
|
6
|
+
-- 1. The per-window budget was counted by RAW project_path string equality.
|
|
7
|
+
-- `/Code/agents/momento-mori` and `/Code/agents/momento-mori/apps/cli`
|
|
8
|
+
-- are one repo and resolve to ONE projects row, but each drew its own
|
|
9
|
+
-- allowance and each hit exactly 5/5 in the same 7 days — the repo
|
|
10
|
+
-- adopted 10 rules under a policy that intends 5. Any repo whose
|
|
11
|
+
-- sessions start in a subdirectory has as many budgets as it has
|
|
12
|
+
-- subdirectories. Fixed in auto-accept-eligibility.ts by counting
|
|
13
|
+
-- through instruction_suggestions.project_id; nothing to do here.
|
|
14
|
+
--
|
|
15
|
+
-- 2. The budget was a compile-time constant, so raising it for one noisy
|
|
16
|
+
-- repo meant a deploy. It is now a setting: NULL columns here mean "use
|
|
17
|
+
-- the shipped default", per-project overrides live in
|
|
18
|
+
-- projects.metadata.autoAcceptLimits and win over these.
|
|
19
|
+
--
|
|
20
|
+
-- 3. THIS producer only ever queued a run when the project held a
|
|
21
|
+
-- suggestion scoring >= 0.8. But GET /api/instruction-suggestions/
|
|
22
|
+
-- auto-acceptable deliberately exempts insights-authored rows from the
|
|
23
|
+
-- score gate (they are filed at 0.5 by the nomination endpoint). So in a
|
|
24
|
+
-- project whose only proposals came from insights, the authoritative
|
|
25
|
+
-- gate said "eligible" and this function never queued the job that would
|
|
26
|
+
-- have asked it. Every insights nomination in such a project was
|
|
27
|
+
-- unreachable — not declined, not surfaced, just never considered.
|
|
28
|
+
|
|
29
|
+
-- ── Settable budget ─────────────────────────────────────────────────────────
|
|
30
|
+
alter table public.profiles
|
|
31
|
+
add column if not exists auto_accept_min_score real,
|
|
32
|
+
add column if not exists auto_accept_max_per_run integer,
|
|
33
|
+
add column if not exists auto_accept_max_per_window integer,
|
|
34
|
+
add column if not exists auto_accept_window_days integer;
|
|
35
|
+
|
|
36
|
+
comment on column public.profiles.auto_accept_min_score is
|
|
37
|
+
'Account override for the auto-accept score gate. NULL = shipped default (0.8). Bounded 0..1 by the API on read AND write — a settable brake still has to be a brake.';
|
|
38
|
+
comment on column public.profiles.auto_accept_max_per_run is
|
|
39
|
+
'Account override for auto-accepts per nightly run per project. NULL = shipped default (3). Bounded 0..25.';
|
|
40
|
+
comment on column public.profiles.auto_accept_max_per_window is
|
|
41
|
+
'Account override for auto-accepts per project per window. NULL = shipped default (15). Bounded 0..100. The brake on CLAUDE.md bloat: an over-large CLAUDE.md measured -22.1% (stratified, significant).';
|
|
42
|
+
comment on column public.profiles.auto_accept_window_days is
|
|
43
|
+
'Account override for the budget window. NULL = shipped default (7). Bounded 1..90.';
|
|
44
|
+
|
|
45
|
+
-- ── Producer ────────────────────────────────────────────────────────────────
|
|
46
|
+
-- Same cheap-gates-only contract as 00170: this decides whether to ASK, never
|
|
47
|
+
-- whether to adopt. The authoritative check stays server-side.
|
|
48
|
+
create or replace function public.enqueue_auto_accept_suggestions()
|
|
49
|
+
returns integer
|
|
50
|
+
language plpgsql
|
|
51
|
+
security definer
|
|
52
|
+
set search_path = public
|
|
53
|
+
as $$
|
|
54
|
+
declare
|
|
55
|
+
queued integer;
|
|
56
|
+
begin
|
|
57
|
+
with eligible as (
|
|
58
|
+
select s.user_id, s.project_path
|
|
59
|
+
from instruction_suggestions s
|
|
60
|
+
join profiles p on p.id = s.user_id
|
|
61
|
+
join instructions i on i.id = s.instruction_id
|
|
62
|
+
left join projects proj on proj.id = s.project_id
|
|
63
|
+
where
|
|
64
|
+
s.kind = 'add'
|
|
65
|
+
-- Score OR provenance. Insights nominations are filed at 0.5 by
|
|
66
|
+
-- POST /api/insights/recommendations/nominate and exempted from the
|
|
67
|
+
-- score gate by the authoritative endpoint; requiring 0.8 here made
|
|
68
|
+
-- that exemption unreachable in any project without a high-scoring
|
|
69
|
+
-- catalog proposal. The per-window budget still bounds what a run may
|
|
70
|
+
-- take, so a permissive trigger cannot become a permissive adoption.
|
|
71
|
+
and (s.score >= 0.8 or i.frontmatter->>'source' = 'insights')
|
|
72
|
+
-- Instruction bodies only, mirroring the endpoint: a skill pasted into
|
|
73
|
+
-- CLAUDE.md is a category error, and queueing a job that can only refuse
|
|
74
|
+
-- is how the loop reported "nothing eligible" for three days.
|
|
75
|
+
and i.type = 'instruction'
|
|
76
|
+
and i.archived = false
|
|
77
|
+
and s.accepted_at is null
|
|
78
|
+
and s.dismissed_at is null
|
|
79
|
+
and s.declined_at is null
|
|
80
|
+
and s.is_stale = false
|
|
81
|
+
-- Consent: per-project override wins, else the user default (true).
|
|
82
|
+
and coalesce(
|
|
83
|
+
proj.metadata->>'autoAcceptSuggestions',
|
|
84
|
+
p.auto_accept_suggestions::text
|
|
85
|
+
) = 'true'
|
|
86
|
+
-- One in flight per (user, project), per the 00083/00086 pattern.
|
|
87
|
+
and not exists (
|
|
88
|
+
select 1 from agent_jobs j
|
|
89
|
+
where j.task_kind = 'cron_auto_accept_suggestions'
|
|
90
|
+
and j.status in ('pending', 'claimed', 'running')
|
|
91
|
+
and j.payload->>'projectPath' = s.project_path
|
|
92
|
+
and j.user_id = s.user_id
|
|
93
|
+
)
|
|
94
|
+
group by s.user_id, s.project_path
|
|
95
|
+
),
|
|
96
|
+
ins as (
|
|
97
|
+
insert into agent_jobs (user_id, task_kind, status, payload, dedupe_key)
|
|
98
|
+
select e.user_id,
|
|
99
|
+
'cron_auto_accept_suggestions',
|
|
100
|
+
'pending',
|
|
101
|
+
jsonb_build_object('projectPath', e.project_path),
|
|
102
|
+
'auto-accept:' || e.user_id::text || ':' || e.project_path
|
|
103
|
+
from eligible e
|
|
104
|
+
returning 1
|
|
105
|
+
)
|
|
106
|
+
select count(*)::int into queued from ins;
|
|
107
|
+
|
|
108
|
+
return queued;
|
|
109
|
+
end;
|
|
110
|
+
$$;
|
|
111
|
+
|
|
112
|
+
comment on function public.enqueue_auto_accept_suggestions() is
|
|
113
|
+
'Nightly producer for cron_auto_accept_suggestions. One row per (user, project) holding at least one consented ADD proposal that is either high-confidence (>= 0.8) or insights-authored, and whose instruction is a non-archived instruction body. Cheap gates only — the authoritative eligibility check is GET /api/instruction-suggestions/auto-acceptable, which also applies the per-project budget. Budget overrides: profiles.auto_accept_max_per_window (account) and projects.metadata.autoAcceptLimits (project). Turn it off for one repo: update projects set metadata = coalesce(metadata,''{}''::jsonb) || ''{"autoAcceptSuggestions":false}''::jsonb where id = ...; or globally: update profiles set auto_accept_suggestions = false where id = ...';
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
-- Randomised measurement gets a throughput dial.
|
|
2
|
+
--
|
|
3
|
+
-- ## The complaint, and what actually caused it
|
|
4
|
+
--
|
|
5
|
+
-- "One a day per project takes forever." Correct, and it was not the
|
|
6
|
+
-- concurrency ceiling that caused it. Two constants in
|
|
7
|
+
-- `routes/instruction-suggestions/loop.ts` compose badly:
|
|
8
|
+
--
|
|
9
|
+
-- TRIAL_MAX_CONCURRENT = 3 how many rules may be under measurement at once
|
|
10
|
+
-- TRIAL_MAX_PER_RUN = 1 how many NEW trials one cycle may start
|
|
11
|
+
--
|
|
12
|
+
-- The producer runs nightly, so `per run` is really `per night`. Starting from
|
|
13
|
+
-- zero, a project needs THREE NIGHTS to reach a ceiling of three — the first
|
|
14
|
+
-- two nights leave measurement capacity deliberately idle. That is pure
|
|
15
|
+
-- latency: it buys no statistical protection, because the steady state it
|
|
16
|
+
-- ramps toward is the same either way.
|
|
17
|
+
--
|
|
18
|
+
-- The ceiling is a real trade and stays put by default: every running
|
|
19
|
+
-- experiment withholds its rule from ~half that project's sessions, so
|
|
20
|
+
-- concurrency is context the agent does not get. The RAMP is not a trade at
|
|
21
|
+
-- all. So the default for per-run rises to fill the ceiling in one cycle
|
|
22
|
+
-- (3 nights -> 1), and both numbers become settings for anyone who wants to
|
|
23
|
+
-- spend more context to answer more questions at once.
|
|
24
|
+
--
|
|
25
|
+
-- What this does NOT touch: GRADUATION_MIN_N (25 measured sessions per arm).
|
|
26
|
+
-- Sessions are the scarce resource and that gate is what keeps a verdict from
|
|
27
|
+
-- enshrining noise. Testing faster must mean asking more questions in
|
|
28
|
+
-- parallel, never accepting thinner evidence for each.
|
|
29
|
+
--
|
|
30
|
+
-- NULL = use the shipped default, so an untouched account tracks changes to
|
|
31
|
+
-- the default instead of freezing today's value at signup. Per-project
|
|
32
|
+
-- overrides live in `projects.metadata.trialLimits` and win over these.
|
|
33
|
+
-- Mirrors 00184's auto-accept budget settings exactly.
|
|
34
|
+
|
|
35
|
+
ALTER TABLE public.profiles
|
|
36
|
+
ADD COLUMN IF NOT EXISTS trial_max_concurrent integer,
|
|
37
|
+
ADD COLUMN IF NOT EXISTS trial_max_per_run integer;
|
|
38
|
+
|
|
39
|
+
COMMENT ON COLUMN public.profiles.trial_max_concurrent IS
|
|
40
|
+
'Account override for how many instruction experiments may run at once on one project. NULL = shipped default (3). Each running experiment withholds its rule from ~half that project''s sessions, so this is a context-vs-questions trade. Per-project override: projects.metadata.trialLimits.maxConcurrent.';
|
|
41
|
+
|
|
42
|
+
COMMENT ON COLUMN public.profiles.trial_max_per_run IS
|
|
43
|
+
'Account override for how many NEW experiments one training cycle may start. NULL = shipped default (3, i.e. fill the concurrency ceiling in a single run). Raising this shortens ramp latency only; it cannot exceed the concurrency ceiling, which is what actually bounds context withholding. Per-project override: projects.metadata.trialLimits.maxPerRun.';
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
-- One authority for "which path is this project", and it may never be a dead one.
|
|
2
|
+
--
|
|
3
|
+
-- THE DEFECT (measured against production 2026-08-13).
|
|
4
|
+
--
|
|
5
|
+
-- Five call sites resolved a project's canonical checkout with the same
|
|
6
|
+
-- hand-copied rule: `distinct on (project_id) ... order by is_worktree asc,
|
|
7
|
+
-- last_seen desc`. Non-worktree first, most-recent second. `project_checkouts`
|
|
8
|
+
-- for momento-mori holds three rows:
|
|
9
|
+
--
|
|
10
|
+
-- /Users/nickyeager/Code/agents/momento-mori is_worktree=TRUE 2026-08-10
|
|
11
|
+
-- /Users/nickyeager/Code/agents/momento-mori/.worktrees/… is_worktree=true 2026-05-04
|
|
12
|
+
-- /Users/nickyeager/Code/agents/momento-mori/apps/cli is_worktree=false 2026-05-01
|
|
13
|
+
--
|
|
14
|
+
-- Session ingest upserts `is_worktree` from each session's own metadata on
|
|
15
|
+
-- every create (routes/sessions/ingest/create.ts), so ONE session that reported
|
|
16
|
+
-- `isWorktree: true` while its projectPath was the repo root flipped the root's
|
|
17
|
+
-- flag. `is_worktree ASC` then outranks recency absolutely, and the crown went
|
|
18
|
+
-- to `apps/cli` — a path no session has rendered from since 2026-05-01.
|
|
19
|
+
--
|
|
20
|
+
-- Consequences, all silent, all of them looking like "nothing eligible":
|
|
21
|
+
--
|
|
22
|
+
-- * `regenerate_insights_jobs` analysed apps/cli every 6h from 2026-08-10
|
|
23
|
+
-- onward — 136 completed jobs re-deriving the same May-era conclusions
|
|
24
|
+
-- ("Across 13 CLI sessions…") while the repo they describe moved on.
|
|
25
|
+
-- * The insights-authored suggestions those runs produce are filed under
|
|
26
|
+
-- project_path=…/apps/cli, so `cron_auto_accept_suggestions` writes
|
|
27
|
+
-- apps/cli/CLAUDE.md and reports `nothing-eligible` for the repo root
|
|
28
|
+
-- every single night.
|
|
29
|
+
-- * 00175 pinned trial nomination to this same rule. Its own comment
|
|
30
|
+
-- diagnoses the apps/cli experiment as undead ("no session has rendered
|
|
31
|
+
-- from apps/cli in months") and then adopts the rule that elects it.
|
|
32
|
+
--
|
|
33
|
+
-- tinyworlds is mis-routed identically, to `…/tinyworlds/src` (2026-05-04).
|
|
34
|
+
--
|
|
35
|
+
-- THE RULE. Recency outranks the worktree flag, but only when the gap is
|
|
36
|
+
-- material: a path may not win if it is more than 30 days staler than the
|
|
37
|
+
-- freshest path that project has. Stated against the project's OWN freshest
|
|
38
|
+
-- path rather than against `now()`, so a project dormant for a year still
|
|
39
|
+
-- resolves to the checkout it was last worked in instead of falling back to
|
|
40
|
+
-- whichever dead sibling happens to carry is_worktree=false.
|
|
41
|
+
--
|
|
42
|
+
-- Within a staleness tier the original intent survives untouched: a real
|
|
43
|
+
-- checkout beats a worktree, and the most recently seen breaks the tie.
|
|
44
|
+
--
|
|
45
|
+
-- This is a VIEW rather than a sixth copy of the ORDER BY on purpose. The
|
|
46
|
+
-- rule has now been wrong in five places at once; the repo's own recorded
|
|
47
|
+
-- lesson for two-copies drift (00177's comment) is that the copies disagree
|
|
48
|
+
-- in the silent direction. TypeScript callers select from this view too.
|
|
49
|
+
|
|
50
|
+
create or replace view public.project_canonical_checkouts as
|
|
51
|
+
select distinct on (project_id)
|
|
52
|
+
project_id,
|
|
53
|
+
path,
|
|
54
|
+
is_worktree,
|
|
55
|
+
last_seen,
|
|
56
|
+
-- Exposed so callers can SAY the path is stale rather than silently
|
|
57
|
+
-- acting on it. /api/loop/status renders this; a canonical path that
|
|
58
|
+
-- has gone quiet while the project has not is how this defect hid.
|
|
59
|
+
(last_seen < freshest - interval '30 days') as is_stale,
|
|
60
|
+
freshest as project_last_seen
|
|
61
|
+
from (
|
|
62
|
+
select pc.project_id,
|
|
63
|
+
pc.path,
|
|
64
|
+
pc.is_worktree,
|
|
65
|
+
pc.last_seen,
|
|
66
|
+
max(pc.last_seen) over (partition by pc.project_id) as freshest
|
|
67
|
+
from public.project_checkouts pc
|
|
68
|
+
where pc.path is not null
|
|
69
|
+
and pc.path <> ''
|
|
70
|
+
) q
|
|
71
|
+
order by project_id,
|
|
72
|
+
(last_seen < freshest - interval '30 days') asc, -- live before stale
|
|
73
|
+
is_worktree asc, -- checkout before worktree
|
|
74
|
+
last_seen desc; -- then most recent
|
|
75
|
+
|
|
76
|
+
comment on view public.project_canonical_checkouts is
|
|
77
|
+
'The single authority for a project''s canonical checkout path. Ranks live paths above ones more than 30 days staler than the project''s freshest, THEN non-worktree, THEN most-recently-seen. Replaces the hand-copied `order by is_worktree asc, last_seen desc` in regenerate_insights_jobs, enqueue_instruction_training, routes/insights/jobs.ts and routes/sessions/query.ts (x3), which elected a path dead since 2026-05-01 for momento-mori and tinyworlds because one session flipped the live root''s is_worktree flag. See 00186.';
|
|
78
|
+
|
|
79
|
+
grant select on public.project_canonical_checkouts to authenticated, service_role;
|
|
80
|
+
|
|
81
|
+
-- ── The insights producer ───────────────────────────────────────────────────
|
|
82
|
+
-- Body unchanged from 00176 except that `canonical` now selects from the view.
|
|
83
|
+
|
|
84
|
+
create or replace function public.regenerate_insights_jobs(
|
|
85
|
+
p_min_sessions integer default 3,
|
|
86
|
+
p_freshness_hours integer default 6
|
|
87
|
+
)
|
|
88
|
+
returns text
|
|
89
|
+
language plpgsql
|
|
90
|
+
security definer
|
|
91
|
+
set search_path = public
|
|
92
|
+
as $$
|
|
93
|
+
declare
|
|
94
|
+
v_inserted int := 0;
|
|
95
|
+
v_skipped_fresh int := 0;
|
|
96
|
+
v_skipped_queued int := 0;
|
|
97
|
+
v_reclaimed int := 0;
|
|
98
|
+
v_id uuid;
|
|
99
|
+
r record;
|
|
100
|
+
begin
|
|
101
|
+
for r in
|
|
102
|
+
with eligible as (
|
|
103
|
+
select s.user_id, s.project_id
|
|
104
|
+
from sessions s
|
|
105
|
+
where s.project_id is not null
|
|
106
|
+
group by s.user_id, s.project_id
|
|
107
|
+
having count(*) >= p_min_sessions
|
|
108
|
+
)
|
|
109
|
+
select e.user_id, e.project_id, c.path as project_path
|
|
110
|
+
from eligible e
|
|
111
|
+
join public.project_canonical_checkouts c on c.project_id = e.project_id
|
|
112
|
+
-- ACTIVITY since this project's last analysis — start, progress, or end.
|
|
113
|
+
where exists (
|
|
114
|
+
select 1 from sessions s2
|
|
115
|
+
where s2.user_id = e.user_id
|
|
116
|
+
and s2.project_id = e.project_id
|
|
117
|
+
and greatest(
|
|
118
|
+
s2.started_at,
|
|
119
|
+
coalesce(s2.ended_at, s2.started_at)
|
|
120
|
+
) > coalesce(
|
|
121
|
+
(select max(j.completed_at)
|
|
122
|
+
from insights_jobs j
|
|
123
|
+
where j.user_id = e.user_id
|
|
124
|
+
and j.project_path = c.path
|
|
125
|
+
and j.status = 'completed'),
|
|
126
|
+
now() - interval '30 days'
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
loop
|
|
130
|
+
if exists (
|
|
131
|
+
select 1 from insights_jobs j
|
|
132
|
+
where j.user_id = r.user_id
|
|
133
|
+
and j.project_path = r.project_path
|
|
134
|
+
and j.status = 'completed'
|
|
135
|
+
and j.completed_at > now() - (p_freshness_hours || ' hours')::interval
|
|
136
|
+
) then
|
|
137
|
+
v_skipped_fresh := v_skipped_fresh + 1;
|
|
138
|
+
continue;
|
|
139
|
+
end if;
|
|
140
|
+
|
|
141
|
+
update insights_jobs j
|
|
142
|
+
set status = 'failed',
|
|
143
|
+
completed_at = now(),
|
|
144
|
+
error = 'reclaimed by regenerate_insights_jobs: queued/in-flight >24h with no progress — worker likely died before any state write'
|
|
145
|
+
where j.user_id = r.user_id
|
|
146
|
+
and j.project_path = r.project_path
|
|
147
|
+
and j.status in ('pending', 'claimed', 'running')
|
|
148
|
+
and j.created_at < now() - interval '24 hours';
|
|
149
|
+
if found then
|
|
150
|
+
v_reclaimed := v_reclaimed + 1;
|
|
151
|
+
end if;
|
|
152
|
+
|
|
153
|
+
if exists (
|
|
154
|
+
select 1 from insights_jobs j
|
|
155
|
+
where j.user_id = r.user_id
|
|
156
|
+
and j.project_path = r.project_path
|
|
157
|
+
and j.status in ('pending', 'claimed', 'running')
|
|
158
|
+
) then
|
|
159
|
+
v_skipped_queued := v_skipped_queued + 1;
|
|
160
|
+
continue;
|
|
161
|
+
end if;
|
|
162
|
+
|
|
163
|
+
v_id := gen_random_uuid();
|
|
164
|
+
insert into insights_jobs (id, user_id, project_id, status, project_path, task_kind, payload)
|
|
165
|
+
values (
|
|
166
|
+
v_id,
|
|
167
|
+
r.user_id,
|
|
168
|
+
r.project_id,
|
|
169
|
+
'pending',
|
|
170
|
+
r.project_path,
|
|
171
|
+
'process_insights',
|
|
172
|
+
jsonb_build_object('mirrorId', v_id, 'projectPath', r.project_path)
|
|
173
|
+
);
|
|
174
|
+
v_inserted := v_inserted + 1;
|
|
175
|
+
end loop;
|
|
176
|
+
|
|
177
|
+
return format(
|
|
178
|
+
'regenerated: %s inserted, %s skipped (fresh), %s skipped (queued), %s reclaimed (stale)',
|
|
179
|
+
v_inserted, v_skipped_fresh, v_skipped_queued, v_reclaimed
|
|
180
|
+
);
|
|
181
|
+
end;
|
|
182
|
+
$$;
|
|
183
|
+
|
|
184
|
+
comment on function public.regenerate_insights_jobs(integer, integer) is
|
|
185
|
+
'Every-6h producer for process_insights. Enqueues one job per (user, project) with >= p_min_sessions sessions and activity since that project''s last completed analysis. As of 00186 the canonical path comes from public.project_canonical_checkouts, so the analysis can no longer be aimed at a checkout dead for months.';
|
|
186
|
+
|
|
187
|
+
-- ── The training producer ───────────────────────────────────────────────────
|
|
188
|
+
-- Body unchanged from 00175 except that `canonical` now selects from the view.
|
|
189
|
+
-- The settle half stays ungated and per-path so off-canonical experiments
|
|
190
|
+
-- retire rather than orphan.
|
|
191
|
+
|
|
192
|
+
create or replace function public.enqueue_instruction_training(p_active_days integer default 7)
|
|
193
|
+
returns integer
|
|
194
|
+
language plpgsql
|
|
195
|
+
security definer
|
|
196
|
+
set search_path = public
|
|
197
|
+
as $$
|
|
198
|
+
declare
|
|
199
|
+
queued integer;
|
|
200
|
+
begin
|
|
201
|
+
with eligible as (
|
|
202
|
+
select s.user_id, s.project_path
|
|
203
|
+
from instruction_suggestions s
|
|
204
|
+
join public.project_canonical_checkouts c on c.path = s.project_path
|
|
205
|
+
where s.kind = 'add'
|
|
206
|
+
and s.accepted_at is null
|
|
207
|
+
and s.dismissed_at is null
|
|
208
|
+
and s.declined_at is null
|
|
209
|
+
and s.is_stale = false
|
|
210
|
+
and exists (
|
|
211
|
+
select 1 from sessions se
|
|
212
|
+
where se.project_id = c.project_id
|
|
213
|
+
and se.user_id = s.user_id
|
|
214
|
+
and se.started_at > now() - (p_active_days || ' days')::interval
|
|
215
|
+
)
|
|
216
|
+
and not exists (
|
|
217
|
+
select 1 from agent_jobs j
|
|
218
|
+
where j.task_kind = 'cron_instruction_training'
|
|
219
|
+
and j.status in ('pending', 'claimed', 'running')
|
|
220
|
+
and j.payload->>'projectPath' = s.project_path
|
|
221
|
+
and j.user_id = s.user_id
|
|
222
|
+
)
|
|
223
|
+
group by s.user_id, s.project_path
|
|
224
|
+
|
|
225
|
+
union
|
|
226
|
+
|
|
227
|
+
select e.user_id, e.project_path
|
|
228
|
+
from instruction_experiments e
|
|
229
|
+
where e.status = 'running'
|
|
230
|
+
and not exists (
|
|
231
|
+
select 1 from agent_jobs j
|
|
232
|
+
where j.task_kind = 'cron_instruction_training'
|
|
233
|
+
and j.status in ('pending', 'claimed', 'running')
|
|
234
|
+
and j.payload->>'projectPath' = e.project_path
|
|
235
|
+
and j.user_id = e.user_id
|
|
236
|
+
)
|
|
237
|
+
group by e.user_id, e.project_path
|
|
238
|
+
),
|
|
239
|
+
ins as (
|
|
240
|
+
insert into agent_jobs (user_id, task_kind, status, payload, dedupe_key)
|
|
241
|
+
select e.user_id,
|
|
242
|
+
'cron_instruction_training',
|
|
243
|
+
'pending',
|
|
244
|
+
jsonb_build_object('projectPath', e.project_path),
|
|
245
|
+
'instruction-training:' || e.user_id::text || ':' || e.project_path
|
|
246
|
+
from eligible e
|
|
247
|
+
returning 1
|
|
248
|
+
)
|
|
249
|
+
select count(*)::int into queued from ins;
|
|
250
|
+
|
|
251
|
+
return queued;
|
|
252
|
+
end;
|
|
253
|
+
$$;
|
|
254
|
+
|
|
255
|
+
comment on function public.enqueue_instruction_training(integer) is
|
|
256
|
+
'Nightly producer for cron_instruction_training. The TRIAL half nominates only on a project''s canonical checkout path with a session inside p_active_days (default 7); SETTLE stays ungated and per-path so existing off-path experiments retire rather than orphan. As of 00186 canonical comes from public.project_canonical_checkouts — 00175 pinned this to a rule that elected the very apps/cli path its own comment describes as undead.';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
create table public.recommendation_applications (
|
|
2
|
+
id uuid primary key default gen_random_uuid(),
|
|
3
|
+
user_id uuid not null references public.profiles(id) on delete cascade,
|
|
4
|
+
instruction_id uuid not null references public.instructions(id) on delete cascade,
|
|
5
|
+
project_id uuid references public.projects(id) on delete set null,
|
|
6
|
+
project_path text not null,
|
|
7
|
+
harness text not null check (harness in ('claude_code', 'codex', 'shared')),
|
|
8
|
+
target_path text not null,
|
|
9
|
+
status text not null default 'proposed' check (status in ('proposed', 'ready', 'applied', 'measuring', 'improved', 'inconclusive', 'harmful', 'rollback_ready', 'reverted', 'failed')),
|
|
10
|
+
apply_mode text not null check (apply_mode in ('auto', 'human')),
|
|
11
|
+
before_hash text,
|
|
12
|
+
applied_hash text,
|
|
13
|
+
applied_at timestamptz,
|
|
14
|
+
measurement_started_at timestamptz,
|
|
15
|
+
measurement_ended_at timestamptz,
|
|
16
|
+
verdict text check (verdict is null or verdict in ('improved', 'inconclusive', 'harmful')),
|
|
17
|
+
verdict_detail text,
|
|
18
|
+
reverted_at timestamptz,
|
|
19
|
+
created_at timestamptz not null default now(),
|
|
20
|
+
updated_at timestamptz not null default now(),
|
|
21
|
+
unique (user_id, instruction_id, project_path, harness, target_path)
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
create index idx_recommendation_applications_user_status on public.recommendation_applications(user_id, status, updated_at desc);
|
|
25
|
+
create index idx_recommendation_applications_user_harness on public.recommendation_applications(user_id, harness, updated_at desc);
|
|
26
|
+
create index idx_recommendation_applications_project on public.recommendation_applications(project_id, updated_at desc);
|
|
27
|
+
|
|
28
|
+
alter table public.recommendation_applications enable row level security;
|
|
29
|
+
create policy "Users manage own recommendation applications"
|
|
30
|
+
on public.recommendation_applications for all
|
|
31
|
+
using (auth.uid() = user_id)
|
|
32
|
+
with check (auth.uid() = user_id);
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
-- The training producer's ACTIVE gate passes for paths nobody works in.
|
|
2
|
+
--
|
|
3
|
+
-- ## Measured 2026-08-14
|
|
4
|
+
--
|
|
5
|
+
-- 00174 added the gate that stops undead trials, and it resolves
|
|
6
|
+
-- `suggestion.project_path → project_checkouts → project_id → sessions`. That
|
|
7
|
+
-- last hop is the defect: it asks "has this PROJECT been active?", not "has
|
|
8
|
+
-- this PATH been active?". Every checkout of an active repo therefore passes,
|
|
9
|
+
-- including ones with no session in months.
|
|
10
|
+
--
|
|
11
|
+
-- Concretely, `/Users/nickyeager/Code/agents/momento-mori/apps/cli` maps to the
|
|
12
|
+
-- momento-mori project, whose ROOT is worked in daily, so the subpath cleared
|
|
13
|
+
-- the gate and the producer enqueued a training job for it every night. Those
|
|
14
|
+
-- jobs nominated real trials against a checkout with no session since
|
|
15
|
+
-- 2026-05-01: 6 running experiments were sitting on it tonight, holding slots,
|
|
16
|
+
-- unable to ever enroll an arm — the exact "undead" failure 00174 was written
|
|
17
|
+
-- to end, arriving through the one hop it did not close. (They were stopped by
|
|
18
|
+
-- hand under `inactive_project`; this stops them being created again.)
|
|
19
|
+
--
|
|
20
|
+
-- ## The rule
|
|
21
|
+
--
|
|
22
|
+
-- A suggestion's path may start a trial only if it IS the project's canonical
|
|
23
|
+
-- checkout. `public.project_canonical_checkouts` (00186) is the single
|
|
24
|
+
-- authority — CLAUDE.md's hard invariant is that this is never re-derived, and
|
|
25
|
+
-- 00174's hand-rolled join is precisely a re-derivation. The view already ranks
|
|
26
|
+
-- live-before-stale, so "canonical" cannot silently mean a dead path.
|
|
27
|
+
--
|
|
28
|
+
-- The per-path activity check is KEPT as well: canonical and recently active
|
|
29
|
+
-- are different claims, and a project that has gone quiet entirely should still
|
|
30
|
+
-- start no new trials.
|
|
31
|
+
--
|
|
32
|
+
-- The SETTLE half stays ungated, unchanged, for 00174's reason: gating it would
|
|
33
|
+
-- orphan an experiment whose project went quiet mid-trial.
|
|
34
|
+
|
|
35
|
+
create or replace function public.enqueue_instruction_training(p_active_days integer default 7)
|
|
36
|
+
returns integer
|
|
37
|
+
language plpgsql
|
|
38
|
+
security definer
|
|
39
|
+
set search_path = public
|
|
40
|
+
as $$
|
|
41
|
+
declare
|
|
42
|
+
queued integer;
|
|
43
|
+
begin
|
|
44
|
+
with eligible as (
|
|
45
|
+
select s.user_id, s.project_path
|
|
46
|
+
from instruction_suggestions s
|
|
47
|
+
where s.kind = 'add'
|
|
48
|
+
and s.accepted_at is null
|
|
49
|
+
and s.dismissed_at is null
|
|
50
|
+
and s.declined_at is null
|
|
51
|
+
and s.is_stale = false
|
|
52
|
+
-- CANONICAL (new in 00188): this path is the project's canonical
|
|
53
|
+
-- checkout, per the view that owns that answer. Without it, every
|
|
54
|
+
-- subdirectory of an active repo is treated as its own live project.
|
|
55
|
+
and exists (
|
|
56
|
+
select 1
|
|
57
|
+
from project_checkouts pc
|
|
58
|
+
join project_canonical_checkouts c on c.project_id = pc.project_id
|
|
59
|
+
where pc.path = s.project_path
|
|
60
|
+
and c.path = s.project_path
|
|
61
|
+
)
|
|
62
|
+
-- ACTIVE (00174, unchanged in intent): the project produced a session
|
|
63
|
+
-- inside the window. An experiment enrolls sessions at render time, so a
|
|
64
|
+
-- project with none coming can only ever produce an undead trial.
|
|
65
|
+
and exists (
|
|
66
|
+
select 1
|
|
67
|
+
from project_checkouts pc
|
|
68
|
+
join sessions se on se.project_id = pc.project_id
|
|
69
|
+
where pc.path = s.project_path
|
|
70
|
+
and se.user_id = s.user_id
|
|
71
|
+
and se.started_at > now() - (p_active_days || ' days')::interval
|
|
72
|
+
)
|
|
73
|
+
and not exists (
|
|
74
|
+
select 1 from agent_jobs j
|
|
75
|
+
where j.task_kind = 'cron_instruction_training'
|
|
76
|
+
and j.status in ('pending', 'claimed', 'running')
|
|
77
|
+
and j.payload->>'projectPath' = s.project_path
|
|
78
|
+
and j.user_id = s.user_id
|
|
79
|
+
)
|
|
80
|
+
group by s.user_id, s.project_path
|
|
81
|
+
|
|
82
|
+
union
|
|
83
|
+
|
|
84
|
+
-- Settle half: deliberately NOT gated (neither on activity nor on
|
|
85
|
+
-- canonicality). One cheap API call per project per night, and gating it
|
|
86
|
+
-- would orphan a running experiment the moment its project went quiet — or,
|
|
87
|
+
-- with the new gate, the moment its checkout stopped being canonical.
|
|
88
|
+
select e.user_id, e.project_path
|
|
89
|
+
from instruction_experiments e
|
|
90
|
+
where e.status = 'running'
|
|
91
|
+
and not exists (
|
|
92
|
+
select 1 from agent_jobs j
|
|
93
|
+
where j.task_kind = 'cron_instruction_training'
|
|
94
|
+
and j.status in ('pending', 'claimed', 'running')
|
|
95
|
+
and j.payload->>'projectPath' = e.project_path
|
|
96
|
+
and j.user_id = e.user_id
|
|
97
|
+
)
|
|
98
|
+
group by e.user_id, e.project_path
|
|
99
|
+
),
|
|
100
|
+
ins as (
|
|
101
|
+
insert into agent_jobs (user_id, task_kind, status, payload, dedupe_key)
|
|
102
|
+
select e.user_id,
|
|
103
|
+
'cron_instruction_training',
|
|
104
|
+
'pending',
|
|
105
|
+
jsonb_build_object('projectPath', e.project_path),
|
|
106
|
+
'instruction-training:' || e.user_id::text || ':' || e.project_path
|
|
107
|
+
from eligible e
|
|
108
|
+
returning 1
|
|
109
|
+
)
|
|
110
|
+
select count(*)::int into queued from ins;
|
|
111
|
+
|
|
112
|
+
return queued;
|
|
113
|
+
end;
|
|
114
|
+
$$;
|
|
115
|
+
|
|
116
|
+
comment on function public.enqueue_instruction_training(integer) is
|
|
117
|
+
'Nightly producer for cron_instruction_training. TRIAL half requires (a) the path IS the project canonical checkout per project_canonical_checkouts (00188 — the project-level activity join in 00174 let every subpath of an active repo through, and apps/cli collected 6 undead trials that way), and (b) a session on the project within p_active_days (default 7). SETTLE half stays ungated so a quiet or non-canonical project cannot orphan a running experiment. Widen deliberately: select enqueue_instruction_training(30).';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
-- 00189: make GET /api/usage/current-limits index-bounded instead of
|
|
2
|
+
-- partition-scanning.
|
|
3
|
+
--
|
|
4
|
+
-- Measured on prod 2026-08-15 with EXPLAIN (ANALYZE, BUFFERS): 133 ms and
|
|
5
|
+
-- 155,494 shared buffer hits PER CALL, x 122,479 calls = 19.0B buffer hits.
|
|
6
|
+
-- That matched the 19.3B in pg_stat_statements exactly and made this the #1
|
|
7
|
+
-- query by total DB time (25.3%) — ahead of realtime WAL decode and far ahead
|
|
8
|
+
-- of the context_snapshots inserts everyone was looking at.
|
|
9
|
+
--
|
|
10
|
+
-- The cost was never the row count of the ANSWER (3 rows). It was that both
|
|
11
|
+
-- LATERAL probes read the ENTIRE (user, provider, source_type, limit_type)
|
|
12
|
+
-- partition — 37,946 index tuples x 3 limit types x 2 probes = 227,676 tuples
|
|
13
|
+
-- to return 3 rows:
|
|
14
|
+
-- * probe 1 is `max(resets_at)`, an aggregate with no LIMIT, so it must visit
|
|
15
|
+
-- every row in the prefix;
|
|
16
|
+
-- * probe 2 filters `resets_at >= <window> - interval '5 minutes'`, which is
|
|
17
|
+
-- NOT sargable against idx_limit_obs_current — that index orders its
|
|
18
|
+
-- trailing column by observed_at, so resets_at is only ever a filter.
|
|
19
|
+
--
|
|
20
|
+
-- Ordering the trailing column by resets_at makes both probes index-bounded:
|
|
21
|
+
-- probe 1 becomes ORDER BY ... LIMIT 1 (one tuple), probe 2 becomes a range
|
|
22
|
+
-- scan over just the current window. NULLS LAST is load-bearing — Postgres
|
|
23
|
+
-- defaults DESC to NULLS FIRST, which would put legacy null-window rows at the
|
|
24
|
+
-- head and make `ORDER BY resets_at DESC LIMIT 1` return NULL instead of the
|
|
25
|
+
-- live window.
|
|
26
|
+
--
|
|
27
|
+
-- INCLUDE (value, observed_at) keeps it index-only: those are the only other
|
|
28
|
+
-- columns the endpoint reads.
|
|
29
|
+
--
|
|
30
|
+
-- idx_limit_obs_current is NOT dropped — the legacy null-window fallback path
|
|
31
|
+
-- still orders by observed_at DESC, and the ingest dedup path uses it too.
|
|
32
|
+
--
|
|
33
|
+
-- CONCURRENTLY; run OUTSIDE a transaction (same convention as 00094/00182).
|
|
34
|
+
|
|
35
|
+
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_limit_obs_window
|
|
36
|
+
ON limit_observations (user_id, provider, source_type, limit_type, resets_at DESC NULLS LAST)
|
|
37
|
+
INCLUDE (value, observed_at);
|