@rulemetric/local 0.12.8 → 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 +687 -389
- package/dist/supabase/migrations/00213_vendor_guidance_snapshots.sql +52 -0
- package/dist/supabase/migrations/00214_host_suspend_recoveries.sql +54 -0
- package/dist/supabase/migrations/00215_worker_presence_gap.sql +29 -0
- package/dist/supabase/migrations/00216_db_headroom_check_and_worker_connections_fk.sql +142 -0
- package/dist/supabase/migrations/00217_harbor_ab_cadence.sql +46 -0
- 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-KIeJWTb-.js → docs-sROR5bQ8.js} +46 -46
- package/dist/web/assets/index-Cpw-uDNX.css +1 -0
- package/dist/web/assets/{index-MYof2fnF.js → index-D7l_eoTZ.js} +43 -43
- package/dist/web/index.html +3 -3
- package/package.json +4 -3
- package/dist/web/assets/index-CFJatBZ3.css +0 -1
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
-- 00213 — vendor guidance snapshots.
|
|
2
|
+
--
|
|
3
|
+
-- One row per DISTINCT extracted text of a registered guidance source
|
|
4
|
+
-- (GUIDANCE_SOURCES in apps/api/src/lib/guidance/sources.ts): Claude Code /
|
|
5
|
+
-- Codex changelogs, docs pages and best-practice posts. Identity is the hash
|
|
6
|
+
-- of the extracted TEXT, not the HTML — blog pages carry per-request nonces
|
|
7
|
+
-- and build ids, and hashing the raw page would file a "new guidance" pitch
|
|
8
|
+
-- every six hours.
|
|
9
|
+
--
|
|
10
|
+
-- `previous_snapshot_id` links a change to what it changed from so the pitch
|
|
11
|
+
-- can carry the added lines; NULL means first sight of the source.
|
|
12
|
+
-- `pitched_at` records that the producer has DEALT with this snapshot — either
|
|
13
|
+
-- materialised it as a pitch (`pitch_instruction_id` names the public
|
|
14
|
+
-- `instructions` row) or deliberately recorded it as a baseline (a changelog's
|
|
15
|
+
-- first snapshot is never pitched; `pitch_instruction_id` stays NULL). The two
|
|
16
|
+
-- are distinguishable on purpose: "nothing to pitch" and "not yet processed"
|
|
17
|
+
-- have repeatedly been reported identically by this pipeline.
|
|
18
|
+
--
|
|
19
|
+
-- Plan: docs/plans/2026-08-29-vendor-guidance-ingestion-plan.md.
|
|
20
|
+
create table if not exists vendor_guidance_snapshots (
|
|
21
|
+
id uuid primary key default gen_random_uuid(),
|
|
22
|
+
fetched_at timestamptz not null default now(),
|
|
23
|
+
source_id text not null,
|
|
24
|
+
harness text not null,
|
|
25
|
+
vendor text not null,
|
|
26
|
+
kind text not null check (kind in ('blog', 'docs', 'changelog')),
|
|
27
|
+
url text not null,
|
|
28
|
+
title text not null,
|
|
29
|
+
content_hash text not null,
|
|
30
|
+
text text not null,
|
|
31
|
+
previous_snapshot_id uuid references vendor_guidance_snapshots(id) on delete set null,
|
|
32
|
+
pitched_at timestamptz,
|
|
33
|
+
pitch_instruction_id uuid references instructions(id) on delete set null
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
create unique index if not exists idx_vendor_guidance_dedup
|
|
37
|
+
on vendor_guidance_snapshots (source_id, content_hash);
|
|
38
|
+
create index if not exists idx_vendor_guidance_source_fetched
|
|
39
|
+
on vendor_guidance_snapshots (source_id, fetched_at desc);
|
|
40
|
+
|
|
41
|
+
alter table vendor_guidance_snapshots enable row level security;
|
|
42
|
+
|
|
43
|
+
drop policy if exists "service role manages vendor guidance" on vendor_guidance_snapshots;
|
|
44
|
+
create policy "service role manages vendor guidance"
|
|
45
|
+
on vendor_guidance_snapshots for all to service_role using (true) with check (true);
|
|
46
|
+
|
|
47
|
+
drop policy if exists "authenticated read vendor guidance" on vendor_guidance_snapshots;
|
|
48
|
+
create policy "authenticated read vendor guidance"
|
|
49
|
+
on vendor_guidance_snapshots for select to authenticated using (true);
|
|
50
|
+
|
|
51
|
+
comment on table vendor_guidance_snapshots is
|
|
52
|
+
'Distinct extracted texts of registered vendor guidance pages (changelogs, docs, blog posts). Producer: apps/api/src/lib/guidance/sync.ts. Each pitched snapshot becomes one public feature instruction + per-(user,project) suggestions at score 0.4 — never auto-adopted (type feature is refused by isAutoAdoptable unconditionally).';
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
-- Two recovery budgets were reading one counter, so the generous one silently
|
|
2
|
+
-- spent the strict one.
|
|
3
|
+
--
|
|
4
|
+
-- 00155 added `lease_recoveries`: a budget for "the process hosting the handler
|
|
5
|
+
-- went away", kept apart from `attempts` ("the handler tried and failed") so a
|
|
6
|
+
-- wedge cannot kill a MAX_ATTEMPTS=1 job. Since then TWO independent paths have
|
|
7
|
+
-- come to spend it, with deliberately different caps:
|
|
8
|
+
--
|
|
9
|
+
-- * the reconciler's lease sweep — the worker VANISHED and the lease lapsed.
|
|
10
|
+
-- MAX_HOST_ABSENT_RECOVERIES = 12 when the presence heartbeat was already
|
|
11
|
+
-- gone at expiry (a suspended laptop), MAX_LEASE_RECOVERIES = 1 otherwise.
|
|
12
|
+
-- Generous, because the handler usually never started: a recovery is free.
|
|
13
|
+
--
|
|
14
|
+
-- * POST /api/work/:id/fail with hostSuspended — the worker SURVIVED the
|
|
15
|
+
-- suspend, unwound, and reported the failure itself, so the reconciler
|
|
16
|
+
-- never sees the row. MAX_HOST_SUSPEND_RECOVERIES = 3. Strict on purpose:
|
|
17
|
+
-- the handler ran and may have spent tokens, so each recovery costs a
|
|
18
|
+
-- partial redo.
|
|
19
|
+
--
|
|
20
|
+
-- Both read `lease_recoveries`, so the strict cap never acted as a per-path
|
|
21
|
+
-- budget. It acted as a gate on the total the OTHER path had already spent: a
|
|
22
|
+
-- row with 3+ reconciler-spent recoveries could never receive a self-report
|
|
23
|
+
-- recovery again, however few self-reported suspends it had had.
|
|
24
|
+
--
|
|
25
|
+
-- Reproduced 2026-08-31 against the real endpoints, two rows identical but for
|
|
26
|
+
-- the counter the reconciler had filled:
|
|
27
|
+
--
|
|
28
|
+
-- prior lease_recoveries = 4 -> hostSuspendRecovery false, status FAILED
|
|
29
|
+
-- prior lease_recoveries = 0 -> requeued, status pending
|
|
30
|
+
--
|
|
31
|
+
-- Four is a third of the reconciler's own budget. So a row it was still happily
|
|
32
|
+
-- carrying died permanently on its FIRST self-reported suspend — the friendlier
|
|
33
|
+
-- signal producing the harsher outcome, at MAX_ATTEMPTS = 1, on exactly the two
|
|
34
|
+
-- kinds the census shows failing this way (cron_auto_accept_suggestions,
|
|
35
|
+
-- cron_reconcile_attribution).
|
|
36
|
+
--
|
|
37
|
+
-- The fix is a second column, not a bigger number: raising
|
|
38
|
+
-- MAX_HOST_SUSPEND_RECOVERIES to 12 would discard the cost distinction that
|
|
39
|
+
-- justifies it, and provenance cannot be recovered from a single counter after
|
|
40
|
+
-- the fact. `lease_recoveries` keeps its meaning and its owner (the reconciler);
|
|
41
|
+
-- the self-report path gets its own.
|
|
42
|
+
|
|
43
|
+
ALTER TABLE eval_jobs ADD COLUMN IF NOT EXISTS host_suspend_recoveries integer NOT NULL DEFAULT 0;
|
|
44
|
+
ALTER TABLE insights_jobs ADD COLUMN IF NOT EXISTS host_suspend_recoveries integer NOT NULL DEFAULT 0;
|
|
45
|
+
ALTER TABLE launch_jobs ADD COLUMN IF NOT EXISTS host_suspend_recoveries integer NOT NULL DEFAULT 0;
|
|
46
|
+
ALTER TABLE session_send_jobs ADD COLUMN IF NOT EXISTS host_suspend_recoveries integer NOT NULL DEFAULT 0;
|
|
47
|
+
ALTER TABLE agent_jobs ADD COLUMN IF NOT EXISTS host_suspend_recoveries integer NOT NULL DEFAULT 0;
|
|
48
|
+
|
|
49
|
+
COMMENT ON COLUMN agent_jobs.host_suspend_recoveries IS
|
|
50
|
+
'Times this row was excused because the worker reported the MACHINE was suspended mid-run (POST /api/work/:id/fail with hostSuspended). Separate from `lease_recoveries`, which the reconciler owns for rows whose lease lapsed: the two budgets have different caps for different reasons, and sharing one counter let the generous one exhaust the strict one. See apps/api/src/routes/work-result.ts.';
|
|
51
|
+
|
|
52
|
+
-- Existing rows start at 0. That is deliberate and safe in the forgiving
|
|
53
|
+
-- direction: a row carrying reconciler-spent recoveries today gets its
|
|
54
|
+
-- self-report budget back, which is precisely the behaviour this restores.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
-- 00215: record the presence heartbeat's OUTAGES, not just its last beat.
|
|
2
|
+
--
|
|
3
|
+
-- `worker_connections.last_heartbeat` is a single upserted column. It answers
|
|
4
|
+
-- "when did this worker last speak", which is a question about NOW. The
|
|
5
|
+
-- reconciler (00155, and the host-absent split of 2026-08-29) needs a question
|
|
6
|
+
-- about the PAST: was the host there when this lease lapsed ten minutes ago?
|
|
7
|
+
--
|
|
8
|
+
-- Comparing the current `last_heartbeat` against a past `lease_expires_at`
|
|
9
|
+
-- cannot answer it. A laptop that suspends, misses the expiry and WAKES before
|
|
10
|
+
-- the reconciler's 5-minute sweep reports a heartbeat newer than the expiry, so
|
|
11
|
+
-- it reads as "alive right through it" — the crash budget (1), not the
|
|
12
|
+
-- host-absent budget (12). The census's own suspends are 2–9 minutes long,
|
|
13
|
+
-- which is exactly the window where the wake lands first.
|
|
14
|
+
--
|
|
15
|
+
-- These two columns are the evidence recorded at the time: the beat before a
|
|
16
|
+
-- silence and the beat that ended it. Only the most recent silence is kept —
|
|
17
|
+
-- the reconciler sweeps within five minutes of an expiry, so the newest outage
|
|
18
|
+
-- is the one that can bracket it. NULL means "no outage observed", which is
|
|
19
|
+
-- what every existing row gets: the reconciler then falls back to exactly the
|
|
20
|
+
-- comparison it makes today, so this migration changes no behaviour on its own.
|
|
21
|
+
|
|
22
|
+
ALTER TABLE public.worker_connections
|
|
23
|
+
ADD COLUMN IF NOT EXISTS presence_gap_started_at timestamptz,
|
|
24
|
+
ADD COLUMN IF NOT EXISTS presence_gap_ended_at timestamptz;
|
|
25
|
+
|
|
26
|
+
COMMENT ON COLUMN public.worker_connections.presence_gap_started_at IS
|
|
27
|
+
'Last presence heartbeat before the most recent observed silence (NULL if none). With presence_gap_ended_at this brackets an outage, so the reconciler can ask whether a lease expiry fell inside it instead of comparing against a heartbeat that has since moved on.';
|
|
28
|
+
COMMENT ON COLUMN public.worker_connections.presence_gap_ended_at IS
|
|
29
|
+
'The heartbeat that ended the most recent observed silence (NULL if none).';
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
-- 2026-09-01 — two "forever" fixes from the prod read-only incident.
|
|
2
|
+
--
|
|
3
|
+
-- INCIDENT. Prod went read-only twice in twelve hours (2026-08-31 ~19:55Z and
|
|
4
|
+
-- 2026-09-01 ~03:53Z): Supabase's disk-threshold read-only mode while the
|
|
5
|
+
-- database sat at 37 GB — 34 GB of it context_snapshots, 23 GB of that INLINE
|
|
6
|
+
-- messages_delta (65,487 rows; 23,196 rows > 256 kB hold 20 GB). Nothing
|
|
7
|
+
-- alarmed: the only readers of pg_database_size were humans running ad-hoc SQL.
|
|
8
|
+
-- Every write in the window failed, including the onboarding cold-start
|
|
9
|
+
-- harness's CLEANUP, which then left an orphan worker_connections row behind
|
|
10
|
+
-- (the auth user was deleted, the row survived: worker_connections has NO
|
|
11
|
+
-- foreign key to profiles at all — three such orphans were found).
|
|
12
|
+
--
|
|
13
|
+
-- FIX 1 — worker_connections gets the FK it always should have had (ON DELETE
|
|
14
|
+
-- CASCADE to profiles), after sweeping the orphans it would reject. A deleted
|
|
15
|
+
-- user can no longer leave a heartbeat row behind, so the cold-start harness's
|
|
16
|
+
-- "cleanup INCOMPLETE — manual sweep needed" can never again be caused by this
|
|
17
|
+
-- table.
|
|
18
|
+
--
|
|
19
|
+
-- FIX 2 — db_headroom_check(): a daily, notifying disk-headroom monitor in the
|
|
20
|
+
-- 00147/00209 pattern (one consolidated notification per superadmin, deduped
|
|
21
|
+
-- while unread). It records db/WAL size daily in db_headroom_metrics so growth
|
|
22
|
+
-- is a queryable series, and fires when usage crosses warn_ratio of
|
|
23
|
+
-- disk_limit_bytes (operator-set from the Supabase dashboard — SQL cannot see
|
|
24
|
+
-- the disk ceiling) OR when 7-day growth exceeds growth_alarm_bytes (fires
|
|
25
|
+
-- even while the limit is unset, so a misconfigured monitor is still a loud
|
|
26
|
+
-- one). Per the conformance rule, apps/api/test/db-headroom.test.ts proves the
|
|
27
|
+
-- check FIRES on a seeded low limit and stays QUIET on a healthy one.
|
|
28
|
+
|
|
29
|
+
-- ───────────────────────── FIX 1: worker_connections FK ─────────────────────
|
|
30
|
+
DELETE FROM worker_connections w
|
|
31
|
+
WHERE NOT EXISTS (SELECT 1 FROM profiles p WHERE p.id = w.user_id);
|
|
32
|
+
|
|
33
|
+
ALTER TABLE worker_connections
|
|
34
|
+
DROP CONSTRAINT IF EXISTS worker_connections_user_id_profiles_fk;
|
|
35
|
+
ALTER TABLE worker_connections
|
|
36
|
+
ADD CONSTRAINT worker_connections_user_id_profiles_fk
|
|
37
|
+
FOREIGN KEY (user_id) REFERENCES profiles(id) ON DELETE CASCADE;
|
|
38
|
+
|
|
39
|
+
-- ───────────────────────── FIX 2: headroom monitor ──────────────────────────
|
|
40
|
+
CREATE TABLE IF NOT EXISTS db_headroom_config (
|
|
41
|
+
id boolean PRIMARY KEY DEFAULT true CHECK (id), -- single row
|
|
42
|
+
disk_limit_bytes bigint, -- NULL until the operator reads the dashboard
|
|
43
|
+
warn_ratio real NOT NULL DEFAULT 0.80,
|
|
44
|
+
growth_alarm_bytes bigint NOT NULL DEFAULT 2147483648, -- 2 GiB per 7 days
|
|
45
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
46
|
+
);
|
|
47
|
+
INSERT INTO db_headroom_config (id) VALUES (true) ON CONFLICT (id) DO NOTHING;
|
|
48
|
+
COMMENT ON TABLE db_headroom_config IS
|
|
49
|
+
'Operator-set disk ceiling for db_headroom_check(). disk_limit_bytes comes from the Supabase dashboard (Database → Disk); SQL cannot observe it. NULL = ratio alarm disabled, growth alarm still active.';
|
|
50
|
+
|
|
51
|
+
CREATE TABLE IF NOT EXISTS db_headroom_metrics (
|
|
52
|
+
measured_at timestamptz NOT NULL DEFAULT now(),
|
|
53
|
+
db_bytes bigint NOT NULL,
|
|
54
|
+
wal_bytes bigint,
|
|
55
|
+
PRIMARY KEY (measured_at)
|
|
56
|
+
);
|
|
57
|
+
COMMENT ON TABLE db_headroom_metrics IS
|
|
58
|
+
'Daily samples written by db_headroom_check() — the growth series behind the headroom alarm and the operator''s "how fast is it filling" question.';
|
|
59
|
+
|
|
60
|
+
CREATE OR REPLACE FUNCTION public.db_headroom_check()
|
|
61
|
+
RETURNS integer
|
|
62
|
+
LANGUAGE plpgsql
|
|
63
|
+
SECURITY DEFINER
|
|
64
|
+
SET search_path TO 'public'
|
|
65
|
+
AS $$
|
|
66
|
+
DECLARE
|
|
67
|
+
cfg db_headroom_config%ROWTYPE;
|
|
68
|
+
v_db bigint;
|
|
69
|
+
v_wal bigint;
|
|
70
|
+
v_prev bigint;
|
|
71
|
+
v_growth bigint;
|
|
72
|
+
v_ratio real;
|
|
73
|
+
v_lines text := '';
|
|
74
|
+
v_created integer := 0;
|
|
75
|
+
BEGIN
|
|
76
|
+
SELECT * INTO cfg FROM db_headroom_config WHERE id = true;
|
|
77
|
+
|
|
78
|
+
v_db := pg_database_size(current_database());
|
|
79
|
+
BEGIN
|
|
80
|
+
SELECT COALESCE(sum(size), 0)::bigint INTO v_wal FROM pg_ls_waldir();
|
|
81
|
+
EXCEPTION WHEN OTHERS THEN
|
|
82
|
+
v_wal := NULL; -- not privileged on every host; the db size alone still alarms
|
|
83
|
+
END;
|
|
84
|
+
|
|
85
|
+
INSERT INTO db_headroom_metrics (measured_at, db_bytes, wal_bytes)
|
|
86
|
+
VALUES (now(), v_db, v_wal)
|
|
87
|
+
ON CONFLICT (measured_at) DO NOTHING;
|
|
88
|
+
|
|
89
|
+
-- 7-day growth: the oldest sample inside the window, else no growth signal yet.
|
|
90
|
+
SELECT db_bytes INTO v_prev
|
|
91
|
+
FROM db_headroom_metrics
|
|
92
|
+
WHERE measured_at >= now() - interval '7 days'
|
|
93
|
+
AND measured_at < now() - interval '6 days'
|
|
94
|
+
ORDER BY measured_at ASC LIMIT 1;
|
|
95
|
+
v_growth := CASE WHEN v_prev IS NULL THEN NULL ELSE v_db - v_prev END;
|
|
96
|
+
|
|
97
|
+
IF cfg.disk_limit_bytes IS NOT NULL AND cfg.disk_limit_bytes > 0 THEN
|
|
98
|
+
v_ratio := v_db::real / cfg.disk_limit_bytes::real;
|
|
99
|
+
IF v_ratio >= cfg.warn_ratio THEN
|
|
100
|
+
v_lines := v_lines || format(E'• database is %s of the %s disk ceiling (%s) — Supabase flips the project READ-ONLY near 95%%; every write then fails\n',
|
|
101
|
+
round((v_ratio * 100)::numeric, 1) || '%', pg_size_pretty(cfg.disk_limit_bytes), pg_size_pretty(v_db));
|
|
102
|
+
END IF;
|
|
103
|
+
END IF;
|
|
104
|
+
|
|
105
|
+
IF v_growth IS NOT NULL AND v_growth >= cfg.growth_alarm_bytes THEN
|
|
106
|
+
v_lines := v_lines || format(E'• database grew %s in 7 days (now %s) — check context_snapshots inline messages_delta and the delta offload\n',
|
|
107
|
+
pg_size_pretty(v_growth), pg_size_pretty(v_db));
|
|
108
|
+
END IF;
|
|
109
|
+
|
|
110
|
+
IF v_lines = '' THEN
|
|
111
|
+
RETURN 0;
|
|
112
|
+
END IF;
|
|
113
|
+
|
|
114
|
+
INSERT INTO notifications (user_id, type, title, body, url, metadata)
|
|
115
|
+
SELECT p.id,
|
|
116
|
+
'db_headroom',
|
|
117
|
+
'Database disk headroom needs attention',
|
|
118
|
+
v_lines || format('WAL: %s. Set db_headroom_config.disk_limit_bytes from the Supabase dashboard if unset.',
|
|
119
|
+
COALESCE(pg_size_pretty(v_wal), 'n/a')),
|
|
120
|
+
'/loop',
|
|
121
|
+
jsonb_build_object('db_bytes', v_db, 'wal_bytes', v_wal, 'growth_7d_bytes', v_growth,
|
|
122
|
+
'disk_limit_bytes', cfg.disk_limit_bytes)
|
|
123
|
+
FROM profiles p
|
|
124
|
+
WHERE p.is_superadmin = true
|
|
125
|
+
AND NOT EXISTS (
|
|
126
|
+
SELECT 1 FROM notifications n
|
|
127
|
+
WHERE n.user_id = p.id AND n.type = 'db_headroom' AND n.read_at IS NULL
|
|
128
|
+
);
|
|
129
|
+
GET DIAGNOSTICS v_created = ROW_COUNT;
|
|
130
|
+
RETURN v_created;
|
|
131
|
+
END;
|
|
132
|
+
$$;
|
|
133
|
+
COMMENT ON FUNCTION public.db_headroom_check() IS
|
|
134
|
+
'Daily disk-headroom monitor (2026-09-01 read-only incident). Samples db/WAL size into db_headroom_metrics; notifies superadmins (deduped while unread) when usage >= warn_ratio of the operator-set disk ceiling or 7-day growth >= growth_alarm_bytes. Returns notifications created.';
|
|
135
|
+
|
|
136
|
+
-- Daily at 05:40 UTC, after the other 05:xx health sweeps. Re-runnable.
|
|
137
|
+
DO $$
|
|
138
|
+
BEGIN
|
|
139
|
+
PERFORM cron.unschedule(jobid) FROM cron.job WHERE jobname = 'db_headroom_check';
|
|
140
|
+
EXCEPTION WHEN OTHERS THEN NULL;
|
|
141
|
+
END $$;
|
|
142
|
+
SELECT cron.schedule('db_headroom_check', '40 5 * * *', $$select public.db_headroom_check();$$);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
-- 00217: the external A/B harness's schedule, as a setting instead of an incantation.
|
|
2
|
+
--
|
|
3
|
+
-- The Harbor harness (scripts/harbor/) measures a rule's effect by running the
|
|
4
|
+
-- same containerised task twice — with and without the instruction — and
|
|
5
|
+
-- comparing pass rates and cost. Until now it ran only when a human typed
|
|
6
|
+
-- `./run.sh`, and its results were written to /tmp, which a reboot deletes.
|
|
7
|
+
--
|
|
8
|
+
-- This column is the schedule. It is a single enum rather than a set of
|
|
9
|
+
-- booleans because every prior scheduling opt-in in this system became a knob
|
|
10
|
+
-- with no surface: the seven `autoEvolve*` flags governed a nightly job allowed
|
|
11
|
+
-- to rewrite CLAUDE.md and `grep autoEvolve apps/web/src` returned nothing
|
|
12
|
+
-- (2026-08-10), and the eval autorun opt-in is documented as a raw UPDATE in a
|
|
13
|
+
-- migration comment. One named, constrained value is readable back to the user;
|
|
14
|
+
-- a spread of booleans is not.
|
|
15
|
+
--
|
|
16
|
+
-- DEFAULT 'off' is load-bearing and is not merely conservative. A sweep spends
|
|
17
|
+
-- the user's own subscription quota and their laptop's CPU spawning agent
|
|
18
|
+
-- processes (~$2.50 and 10-15 minutes per task, measured 2026-09-02 at n=25 per
|
|
19
|
+
-- arm). Nothing that spends a user's money may default to on.
|
|
20
|
+
--
|
|
21
|
+
-- The CHECK is the reason this is a column and not free text: an unrecognised
|
|
22
|
+
-- cadence must fail at write time. A producer that reads a value it does not
|
|
23
|
+
-- understand has two options, run or skip, and both are wrong — running spends
|
|
24
|
+
-- money the user did not ask for, skipping is silent and looks exactly like a
|
|
25
|
+
-- quiet night, which is the failure mode this whole area keeps rediscovering.
|
|
26
|
+
--
|
|
27
|
+
-- Values (see apps/api/src/lib/harbor-settings.ts for the spend and detection
|
|
28
|
+
-- latency attached to each):
|
|
29
|
+
-- off no schedule; still runnable on demand. The default.
|
|
30
|
+
-- weekly one sweep a week over tasks proven to discriminate.
|
|
31
|
+
-- nightly one rotating task per night; buys latency, not statistical power.
|
|
32
|
+
-- on_change only when a measured rule is edited or promoted.
|
|
33
|
+
-- two_speed weekly proven + a monthly pass over measured negatives.
|
|
34
|
+
|
|
35
|
+
ALTER TABLE public.profiles
|
|
36
|
+
ADD COLUMN IF NOT EXISTS harbor_ab_cadence text NOT NULL DEFAULT 'off';
|
|
37
|
+
|
|
38
|
+
ALTER TABLE public.profiles
|
|
39
|
+
DROP CONSTRAINT IF EXISTS profiles_harbor_ab_cadence_check;
|
|
40
|
+
|
|
41
|
+
ALTER TABLE public.profiles
|
|
42
|
+
ADD CONSTRAINT profiles_harbor_ab_cadence_check
|
|
43
|
+
CHECK (harbor_ab_cadence IN ('off', 'weekly', 'nightly', 'on_change', 'two_speed'));
|
|
44
|
+
|
|
45
|
+
COMMENT ON COLUMN public.profiles.harbor_ab_cadence IS
|
|
46
|
+
'Schedule for the external Harbor A/B harness. Default off — a sweep spends the user''s subscription quota and laptop CPU. Enabling is gated on a worker having REPORTED Harbor readiness (docker + harbor + agent CLI); see harbor-settings.ts. Never gate this as a worker capability: that is how DB_REQUIRING_KINDS made features die invisibly for every user outside a repo checkout (#287).';
|
|
@@ -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.';
|