@rulemetric/local 0.12.3 → 0.12.5

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.
@@ -0,0 +1,156 @@
1
+ -- A routine that looks for sessions nobody counted.
2
+ --
3
+ -- THE DEFECT CLASS. The proxy auto-bootstraps a session from LLM traffic before
4
+ -- any hook has run, so it has the wire id but no cwd, and the row lands with
5
+ -- project_path and project_id NULL. Events keep flowing. Nothing errors. But
6
+ -- BOTH insights consumers filter on attribution --
7
+ -- `regenerate_insights_jobs()` selects `where s.project_id is not null`, and
8
+ -- routes/insights/jobs.ts requires it too -- so the session is never counted.
9
+ --
10
+ -- The only symptom is silence: a project simply has no insights. Measured
11
+ -- 2026-08-26 on the cloud DB, 354 sessions / 39,193 events were orphaned this
12
+ -- way, one repo having lost every session it ever had across four days of work,
13
+ -- and it surfaced only because a human asked why that project looked empty.
14
+ -- `create.ts:repairAttribution` stops new sessions from orphaning; this
15
+ -- producer is what finds the ones already on the books, and anything a future
16
+ -- gap lets through.
17
+ --
18
+ -- WHY A WORKER JOB AND NOT A SQL BACKFILL. This function deliberately does no
19
+ -- repairing itself. Repair needs a git remote or root commit, and only 6 of
20
+ -- those 354 rows carried ANY git metadata -- the server has nothing to match
21
+ -- on. Matching on project_path alone is precisely the cwd-only fallback Phase
22
+ -- 12 removed for causing misassignment, and a routine running unattended every
23
+ -- hour must not reintroduce it. The evidence lives on the machine that ran the
24
+ -- session: the checkout is still on disk and `git remote get-url origin` still
25
+ -- answers. So the handler reads the anchors there and POSTs them to
26
+ -- POST /api/sessions, which routes into the same `repairAttribution` the live
27
+ -- hook uses -- one implementation of the match order, two callers.
28
+ --
29
+ -- SCHEDULE. Hourly at :20, off the nightly loop slots (04:30 suggestions ->
30
+ -- 05:00 auto-accept -> 05:30 refine -> 05:45 retire -> 06:10 autorun -> 07:30
31
+ -- evolution). Attribution repair is cheap, reads no LLM, and wants to happen
32
+ -- BEFORE the 6-hourly insights producer at 00/06/12/18 so a session repaired
33
+ -- this hour is counted in the next insights pass rather than waiting a day.
34
+ -- ---------------------------------------------------------------------------
35
+
36
+ create or replace function public.enqueue_reconcile_attribution()
37
+ returns integer
38
+ language plpgsql
39
+ security definer
40
+ set search_path = public
41
+ as $$
42
+ declare
43
+ queued integer;
44
+ begin
45
+ with eligible as (
46
+ select s.user_id
47
+ from public.sessions s
48
+ -- INVISIBLE TO INSIGHTS is wider than project_id IS NULL. This function
49
+ -- INNER JOINs project_canonical_checkouts, which is built from
50
+ -- project_checkouts -- so a project with no checkout row is skipped no
51
+ -- matter how well its sessions are attributed, and a project whose every
52
+ -- session was orphaned has never had one recorded (the insert path only
53
+ -- upserts on a match). Chasing only the NULL half is how the first real
54
+ -- reconcile pass attributed Thrakopontia and left it exactly as unable to
55
+ -- produce insights as before.
56
+ where (
57
+ s.project_id is null
58
+ or not exists (
59
+ select 1 from public.project_checkouts pc where pc.project_id = s.project_id
60
+ )
61
+ )
62
+ -- Only chase orphans a worker could plausibly still repair. A session
63
+ -- whose checkout was deleted years ago is permanently unrepairable, and
64
+ -- re-walking it every hour forever is pure noise. 90 days is well past
65
+ -- Claude Code's transcript retention, which is the recovery source for
66
+ -- the pathless shape.
67
+ and s.started_at > now() - interval '90 days'
68
+ -- Sessions with no external id cannot be addressed through
69
+ -- POST /api/sessions at all (it matches on external_session_id), so the
70
+ -- handler would only count them as unrepairable. Do not enqueue for a
71
+ -- user who has nothing BUT those.
72
+ and s.external_session_id is not null
73
+ -- Temp fixtures and home directories are orphaned CORRECTLY. The handler
74
+ -- refuses them; not enqueuing on their account keeps a guaranteed refusal
75
+ -- out of the hourly feed.
76
+ and (
77
+ s.project_path is null
78
+ or (
79
+ s.project_path !~ '^/(private/)?(tmp|var/folders)'
80
+ and s.project_path !~ '^/(Users|home)/[^/]+/?$'
81
+ )
82
+ )
83
+ group by s.user_id
84
+ ), fresh as (
85
+ select e.user_id
86
+ from eligible e
87
+ -- One in flight at a time, per the 00083/00086 pattern.
88
+ where not exists (
89
+ select 1 from public.agent_jobs j
90
+ where j.task_kind = 'cron_reconcile_attribution'
91
+ and j.status in ('pending', 'claimed', 'running')
92
+ and j.user_id = e.user_id
93
+ )
94
+ -- Do not re-run within the hour for a user whose last pass just completed.
95
+ -- The orphan set only changes when new sessions land or a checkout appears.
96
+ and not exists (
97
+ select 1 from public.agent_jobs j
98
+ where j.task_kind = 'cron_reconcile_attribution'
99
+ and j.user_id = e.user_id
100
+ and j.status = 'completed'
101
+ and j.completed_at > now() - interval '50 minutes'
102
+ )
103
+ ), ins as (
104
+ insert into agent_jobs (user_id, task_kind, status, payload, dedupe_key)
105
+ select f.user_id,
106
+ 'cron_reconcile_attribution',
107
+ 'pending',
108
+ jsonb_build_object('limit', 200),
109
+ 'reconcile-attribution:' || f.user_id
110
+ from fresh f
111
+ returning 1
112
+ )
113
+ select count(*)::int into queued from ins;
114
+
115
+ return queued;
116
+ end;
117
+ $$;
118
+
119
+ comment on function public.enqueue_reconcile_attribution() is
120
+ 'Hourly producer for cron_reconcile_attribution (:20). Enqueues one run per user holding sessions with project_id NULL that a worker could still repair (under 90 days old, has an external_session_id, not a temp/home path). Does NOT repair anything itself: attribution needs git anchors that live on the machine that ran the session, and matching on project_path alone is the cwd-only fallback Phase 12 removed. The handler reads those anchors from disk and POSTs them to POST /api/sessions, where repairAttribution performs the write.';
121
+
122
+ -- Observability: the residual count, so a rising floor of unrepairable orphans
123
+ -- is visible instead of being smoothed away. `repairable_recent` is what the
124
+ -- producer actually chases; `total` is every orphan on the books.
125
+ create or replace view public.session_attribution_health as
126
+ select
127
+ s.user_id,
128
+ count(*)::int as total_unattributed,
129
+ count(*) filter (
130
+ where s.started_at > now() - interval '90 days'
131
+ and s.external_session_id is not null
132
+ and (
133
+ s.project_path is null
134
+ or (
135
+ s.project_path !~ '^/(private/)?(tmp|var/folders)'
136
+ and s.project_path !~ '^/(Users|home)/[^/]+/?$'
137
+ )
138
+ )
139
+ )::int as repairable_recent,
140
+ sum(s.event_count)::bigint as orphaned_events,
141
+ max(s.started_at) as newest_orphan
142
+ from public.sessions s
143
+ where s.project_id is null
144
+ or not exists (
145
+ select 1 from public.project_checkouts pc where pc.project_id = s.project_id
146
+ )
147
+ group by s.user_id;
148
+
149
+ comment on view public.session_attribution_health is
150
+ 'Per-user count of sessions invisible to the insights pipeline (project_id IS NULL). repairable_recent is the subset cron_reconcile_attribution will attempt; a total that keeps climbing while repairable_recent stays near zero means the orphans are structurally unrepairable, not that the reconciler is failing.';
151
+
152
+ select cron.schedule(
153
+ 'enqueue_reconcile_attribution',
154
+ '20 * * * *',
155
+ $$select public.enqueue_reconcile_attribution();$$
156
+ );
@@ -0,0 +1,183 @@
1
+ -- Repair the two data shapes that leave a correctly-attributed project unable
2
+ -- to produce insights.
3
+ --
4
+ -- Both are consequences of the same root cause fixed in code on 2026-08-26: the
5
+ -- subdir-consolidation normalizer in POST /sessions collapsed any path under a
6
+ -- known session path to that parent, treated `/Users/<user>` as a valid parent,
7
+ -- and picked an ARBITRARY first match. So sessions landed with the HOME
8
+ -- DIRECTORY as their project_path, and `upsertProjectCheckout` (rightly)
9
+ -- refuses to record a home dir as a checkout. The project_id was correct the
10
+ -- whole time -- it comes from the git remote at capture -- but with no row in
11
+ -- project_checkouts, `project_canonical_checkouts` is empty for that project
12
+ -- and `regenerate_insights_jobs()` INNER JOINs that view. Attributed, and
13
+ -- invisible.
14
+ --
15
+ -- Measured on prod 2026-08-26:
16
+ -- * kc-content-workspace: 191 sessions, 181 of them at `/Users/kylechalmers`,
17
+ -- ZERO checkouts. Same for kc-ops-workspace (41 sessions, 28 at home).
18
+ -- This is also what 00205's canonical-checkout gate blocks: that user's
19
+ -- auto-accept lane is dead until his checkouts are registered. Registering
20
+ -- them is the fix; widening the gate is not.
21
+ -- * context-engineering-intro exists TWICE -- one row with a git remote and
22
+ -- root commit (4 sessions, no checkout) and one anchorless legacy row (no
23
+ -- remote, no root commit, 1 session) that HOLDS the checkout for the path.
24
+ -- Because project_checkouts has a GLOBAL unique index on `path` while the
25
+ -- upsert conflicts on the (project_id, path) primary key, the real project
26
+ -- could never record its own checkout: 23505, swallowed, forever.
27
+ --
28
+ -- Both halves are written as idempotent FUNCTIONS rather than one-shot DO
29
+ -- blocks so they can be tested, re-run, and called again if the shape recurs.
30
+ -- ---------------------------------------------------------------------------
31
+
32
+ -- ── 1. Merge anchorless duplicate project rows ──────────────────────────────
33
+ --
34
+ -- A project row with NEITHER a git_remote_url NOR a git_root_commit has no
35
+ -- identity anchor at all -- it predates git-context-as-identity and can never
36
+ -- be matched again by capture. When such a row shares a name with a project
37
+ -- that DOES have an anchor, it is the same repo, and everything hanging off it
38
+ -- belongs to the anchored row.
39
+ --
40
+ -- STRICT: only merges when exactly ONE candidate keeper exists for the name.
41
+ -- Two anchored rows for one name is a different problem (a real fork, or a
42
+ -- rename) and must not be resolved by guessing.
43
+ create or replace function public.merge_anchorless_duplicate_projects()
44
+ returns integer
45
+ language plpgsql
46
+ security definer
47
+ set search_path = public
48
+ as $$
49
+ declare
50
+ merged integer := 0;
51
+ r record;
52
+ begin
53
+ for r in
54
+ select dud.id as dud_id, keeper.id as keeper_id
55
+ from public.projects dud
56
+ join lateral (
57
+ select k.id
58
+ from public.projects k
59
+ where k.name = dud.name
60
+ and k.id <> dud.id
61
+ and (k.git_remote_url is not null or k.git_root_commit is not null)
62
+ limit 2
63
+ ) keeper on true
64
+ where dud.git_remote_url is null
65
+ and dud.git_root_commit is null
66
+ -- Exactly one keeper, or we are guessing.
67
+ and (
68
+ select count(*) from public.projects k2
69
+ where k2.name = dud.name and k2.id <> dud.id
70
+ and (k2.git_remote_url is not null or k2.git_root_commit is not null)
71
+ ) = 1
72
+ loop
73
+ -- project_checkouts carries a GLOBAL unique index on `path`, so a straight
74
+ -- repoint collides whenever the keeper already knows the path. Drop the
75
+ -- dud's row in that case; otherwise move it.
76
+ delete from public.project_checkouts c
77
+ where c.project_id = r.dud_id
78
+ and exists (
79
+ select 1 from public.project_checkouts k
80
+ where k.path = c.path and k.project_id = r.keeper_id
81
+ );
82
+ update public.project_checkouts set project_id = r.keeper_id where project_id = r.dud_id;
83
+
84
+ update public.sessions set project_id = r.keeper_id where project_id = r.dud_id;
85
+ update public.insights_jobs set project_id = r.keeper_id where project_id = r.dud_id;
86
+ update public.instruction_suggestions set project_id = r.keeper_id where project_id = r.dud_id;
87
+ update public.agent_runs set project_id = r.keeper_id where project_id = r.dud_id;
88
+ update public.treatment_assignments set project_id = r.keeper_id where project_id = r.dud_id;
89
+ update public.memories set project_id = r.keeper_id where project_id = r.dud_id;
90
+ update public.pending_treatment_exposures set project_id = r.keeper_id where project_id = r.dud_id;
91
+ update public.recommendation_applications set project_id = r.keeper_id where project_id = r.dud_id;
92
+
93
+ -- project_instructions is UNIQUE on (project_id, instruction_id) and
94
+ -- CASCADE-deletes, so drop links the keeper already holds before moving.
95
+ delete from public.project_instructions pi
96
+ where pi.project_id = r.dud_id
97
+ and exists (
98
+ select 1 from public.project_instructions k
99
+ where k.project_id = r.keeper_id and k.instruction_id = pi.instruction_id
100
+ );
101
+ update public.project_instructions set project_id = r.keeper_id where project_id = r.dud_id;
102
+
103
+ delete from public.projects where id = r.dud_id;
104
+ merged := merged + 1;
105
+ end loop;
106
+
107
+ return merged;
108
+ end;
109
+ $$;
110
+
111
+ comment on function public.merge_anchorless_duplicate_projects() is
112
+ 'Merges a projects row that has NEITHER git_remote_url NOR git_root_commit into the single same-named row that does have an anchor, repointing every referencing table first. Such a row predates git-context-as-identity and can never be matched by capture again, but it can still HOLD a project_checkouts row that the real project then cannot record (project_checkouts has a global unique index on path while the upsert conflicts on the (project_id, path) PK) -- which is how context-engineering-intro had two rows, one owning the path and the other owning the sessions. Refuses to act when more than one anchored candidate shares the name.';
113
+
114
+ -- ── 2. Register the checkout an attributed project already demonstrates ─────
115
+ --
116
+ -- NOT attribution inference. `sessions.project_id` is already set here, decided
117
+ -- at capture time by a git remote or root commit; this only records WHICH
118
+ -- DIRECTORY those already-attributed sessions ran in. That distinction is why
119
+ -- this is safe and why the cwd-only fallback removed in Phase 12 is not being
120
+ -- reintroduced: nothing here decides which project a session belongs to.
121
+ --
122
+ -- Refuses: home directories and temp paths (never checkouts), worktree paths
123
+ -- (a worktree is not the canonical root -- 00186/00201 exist because electing
124
+ -- one aims the whole loop at a path nobody works in), and any path already
125
+ -- claimed by another project (the global unique index).
126
+ create or replace function public.backfill_project_checkouts_from_sessions()
127
+ returns integer
128
+ language plpgsql
129
+ security definer
130
+ set search_path = public
131
+ as $$
132
+ declare
133
+ inserted integer;
134
+ begin
135
+ with candidate as (
136
+ select
137
+ s.project_id,
138
+ s.project_path,
139
+ count(*) as sessions,
140
+ row_number() over (
141
+ partition by s.project_id
142
+ -- Most-used real path wins; shallower breaks ties, so a repo root is
143
+ -- preferred over a subdirectory of it.
144
+ order by count(*) desc, length(s.project_path) asc, s.project_path asc
145
+ ) as rank
146
+ from public.sessions s
147
+ where s.project_id is not null
148
+ and s.project_path is not null
149
+ and s.project_path !~ '^/(Users|home)/[^/]+/?$'
150
+ and s.project_path !~ '^/(private/)?(tmp|var/folders)'
151
+ and s.project_path not like '%/.claude/worktrees/%'
152
+ and s.project_path not like '%/.worktrees/%'
153
+ -- Only projects that have NO checkout at all. Never second-guess a
154
+ -- project whose checkouts are already known.
155
+ and not exists (
156
+ select 1 from public.project_checkouts c where c.project_id = s.project_id
157
+ )
158
+ -- Global unique on path: skip anything another project already owns.
159
+ and not exists (
160
+ select 1 from public.project_checkouts c2 where c2.path = s.project_path
161
+ )
162
+ group by s.project_id, s.project_path
163
+ ), ins as (
164
+ insert into public.project_checkouts (project_id, path, is_worktree)
165
+ select c.project_id, c.project_path, false
166
+ from candidate c
167
+ where c.rank = 1
168
+ on conflict do nothing
169
+ returning 1
170
+ )
171
+ select count(*)::int into inserted from ins;
172
+
173
+ return inserted;
174
+ end;
175
+ $$;
176
+
177
+ comment on function public.backfill_project_checkouts_from_sessions() is
178
+ 'Registers one canonical checkout for each ALREADY-ATTRIBUTED project that has none, using the most-used real directory its own sessions ran in. Not attribution inference: sessions.project_id is already set from a git anchor, and this only records which directory those sessions used. Skips home dirs, temp paths, worktree paths, and any path another project already claims. Exists because the POST /sessions path normalizer used to collapse session paths onto the home directory, which upsertProjectCheckout then rightly refused to record -- leaving projects correctly attributed and still invisible to regenerate_insights_jobs(), which INNER JOINs project_canonical_checkouts.';
179
+
180
+ -- Run both once, now. Order matters: merging frees a path that the keeper can
181
+ -- then be given, and the backfill skips projects that already have a checkout.
182
+ select public.merge_anchorless_duplicate_projects();
183
+ select public.backfill_project_checkouts_from_sessions();
@@ -0,0 +1,131 @@
1
+ -- 00208 — ONE candidate set for auto-accept, defined once, read everywhere.
2
+ --
3
+ -- THE PATTERN THIS ENDS. Nine separate gates have now been found between "a
4
+ -- proposal exists" and "the actuator adopts it", and every one of them was
5
+ -- found the same way: adoption looked stopped, somebody hand-wrote a query,
6
+ -- found ONE gate, opened it, and declared the chain clear. #372's own commit
7
+ -- message said "two gates had to open, not one" — there were four. Then five
8
+ -- and six (00198/00199), seven and eight (00203/00204), and the 2026-08-27
9
+ -- census. The mechanism is always the same: the eligibility predicate is
10
+ -- HAND-COPIED (TS endpoint, TS operator count, SQL producer), the copies
11
+ -- drift, and the stage that disagrees reports a number that reads identically
12
+ -- to "there was nothing to do".
13
+ --
14
+ -- This view is the producer's candidate predicate extracted to a single named
15
+ -- object. From now on:
16
+ --
17
+ -- * `enqueue_auto_accept_suggestions()` (below) SELECTS from it — no inline
18
+ -- copy of the predicate survives in the producer.
19
+ -- * `suggestion_pipeline_conformance()` (00209) counts it, so the monitor
20
+ -- and the producer can never disagree about what "candidates exist" means.
21
+ -- * Diagnostic queries (scripts/dev/suggestion-gate-census.sql) can read it
22
+ -- instead of restating the ladder.
23
+ --
24
+ -- WHAT IT DELIBERATELY IS NOT. It is not the endpoint's predicate. The
25
+ -- authority for "may this be written into a file" stays server-side in
26
+ -- `GET /instruction-suggestions/auto-acceptable` (loop.ts), which additionally
27
+ -- applies the SETTABLE score gate (`resolveAutoAcceptLimits`), per-run and
28
+ -- window budgets, placeholder detection, the 30-day actuator-refusal window
29
+ -- and the running-experiment guard. This view is the producer's CHEAP SUBSET —
30
+ -- its job is to avoid queueing work that would refuse, and 0.8 here is the
31
+ -- DEFAULT minScore, hardcoded exactly as 00205 had it. That asymmetry is
32
+ -- recorded, intended, and now monitored: 00209's `endpoint_refuses_everything`
33
+ -- check fires when jobs run against this candidate set and neither adopt nor
34
+ -- log a refusal, which is the observable signature of the two predicates
35
+ -- having drifted apart (the exact #372 failure, as an alarm instead of an
36
+ -- archaeology session).
37
+ --
38
+ -- Grain: one row per candidate SUGGESTION. The producer groups to
39
+ -- (user_id, project_path); the monitor counts rows and users. The in-flight
40
+ -- agent_jobs dedupe stays in the producer — that is queue state, not
41
+ -- eligibility, and a suggestion does not stop being a candidate because a job
42
+ -- is already on its way.
43
+
44
+ create or replace view public.auto_accept_candidate_suggestions as
45
+ select s.id as suggestion_id,
46
+ s.user_id,
47
+ s.project_path,
48
+ s.project_id,
49
+ s.instruction_id,
50
+ s.score,
51
+ i.type as instruction_type,
52
+ s.generated_at
53
+ from instruction_suggestions s
54
+ join profiles p on p.id = s.user_id
55
+ join instructions i on i.id = s.instruction_id
56
+ left join projects proj on proj.id = s.project_id
57
+ where s.kind = 'add'
58
+ -- Default-minScore or insights-authored (owner directive 2026-08-09; the
59
+ -- 0.5-vs-0.8 self-inflicted deadlock is documented in loop.ts:86-98).
60
+ and (s.score >= 0.8 or i.frontmatter->>'source' = 'insights')
61
+ -- The single auto-adoptable set. TS mirror: `isAutoAdoptable()` in
62
+ -- apps/api/src/lib/insights/artifacts/types.ts — the agreement between the
63
+ -- two is asserted by apps/api/test/auto-accept-drift.test.ts.
64
+ and i.type in ('instruction', 'skill')
65
+ and i.archived = false
66
+ and s.accepted_at is null
67
+ and s.dismissed_at is null
68
+ -- Nobody answering is not a refusal (00203); measurement declines still are.
69
+ and (s.declined_at is null or s.declined_reason = 'surface_budget')
70
+ and s.is_stale = false
71
+ -- Consent, project override first (00170 lineage).
72
+ and coalesce(
73
+ proj.metadata->>'autoAcceptSuggestions',
74
+ p.auto_accept_suggestions::text
75
+ ) = 'true'
76
+ -- WHERE an unattended writer may write: the canonical checkout, nowhere
77
+ -- else (00205). Read from the view, never re-derived (00186 invariant).
78
+ and exists (
79
+ select 1 from public.project_canonical_checkouts c
80
+ where c.path = s.project_path
81
+ )
82
+ -- The worker said this directory is not there (00202, 24h-expiring).
83
+ and not public.project_path_recently_missing(s.user_id, s.project_path);
84
+
85
+ grant select on public.auto_accept_candidate_suggestions to authenticated, service_role;
86
+
87
+ comment on view public.auto_accept_candidate_suggestions is
88
+ 'The auto-accept producer''s candidate predicate, extracted so the producer (00208), the conformance monitor (00209) and diagnostics read ONE definition. Cheap subset only — the endpoint (loop.ts /auto-acceptable) remains the authority and adds settable limits, budgets, placeholder and experiment gates.';
89
+
90
+ -- The producer, rewritten to read the view. Behaviour-identical to 00205 by
91
+ -- construction: the predicate moved, the queue-state dedupe stayed. The nine
92
+ -- tests in apps/api/test/auto-accept-producer.test.ts are the proof.
93
+ create or replace function public.enqueue_auto_accept_suggestions()
94
+ returns integer
95
+ language plpgsql
96
+ security definer
97
+ set search_path = public
98
+ as $$
99
+ declare
100
+ queued integer;
101
+ begin
102
+ with eligible as (
103
+ select v.user_id, v.project_path
104
+ from public.auto_accept_candidate_suggestions v
105
+ where not exists (
106
+ select 1 from agent_jobs j
107
+ where j.task_kind = 'cron_auto_accept_suggestions'
108
+ and j.status in ('pending', 'claimed', 'running')
109
+ and j.payload->>'projectPath' = v.project_path
110
+ and j.user_id = v.user_id
111
+ )
112
+ group by v.user_id, v.project_path
113
+ ),
114
+ ins as (
115
+ insert into agent_jobs (user_id, task_kind, status, payload, dedupe_key)
116
+ select e.user_id,
117
+ 'cron_auto_accept_suggestions',
118
+ 'pending',
119
+ jsonb_build_object('projectPath', e.project_path),
120
+ 'auto-accept:' || e.user_id::text || ':' || e.project_path
121
+ from eligible e
122
+ returning 1
123
+ )
124
+ select count(*)::int into queued from ins;
125
+
126
+ return queued;
127
+ end;
128
+ $$;
129
+
130
+ comment on function public.enqueue_auto_accept_suggestions() is
131
+ 'Hourly producer for cron_auto_accept_suggestions. Candidate predicate lives in the auto_accept_candidate_suggestions view (00208) — one definition shared with the conformance monitor. History: 00197 (type gate), 00202 (missing paths), 00203 (silence declines), 00205 (canonical checkouts only).';