Files
oikos/internal/db/queries/operations.sql
dtoro d7b526a112 fix(scheduler): honour check_defs.interval_s, and renumber migrations off main
ListEnabledCheckDefs selected interval_s but never filtered on it, so every
enabled check ran on every 30s pass and the declared per-check intervals were
decorative. Invisible at 17 enabled checks; at ~180 it would have meant ~126
SSH connections every 30s (~363k/day) and `apt update` on every machine every
30 seconds — 14,400 mirror hits a day to answer a question that changes daily.

- check_defs.last_run_at (migration 026) + a due-ness predicate in the query.
  A column rather than scheduler memory because this control plane restarts on
  every deploy, and an in-memory map would re-fire every check on each restart.
- runCheck stamps last_run_at before processing the result, so a permanently
  failing check backs off to its interval instead of re-running every pass.
- updates and backup-freshness drop to daily. Both answer questions whose
  answers change about once a day; 60s was just the shared ssh-script default.
- last_run_at is seeded to a random offset within the interval so checks
  created by the same seed do not stay in lockstep — otherwise ~165 probes
  land in the same instant each minute instead of spread across it.
  Deliberately not in the upsert's DO UPDATE: a re-seed must not re-herd them.

Steady state becomes ~180k SSH/day (down from ~363k) and 5 apt runs/day
(down from 14,400), with each 60s check landing at its own point in the minute.

Also renumbers 022→023, 023→024, 024→025: origin/main added its own
022_knowledge_revisions, and prod has already applied version 22. Left
colliding, prod would have skipped the monitoring_spec migration entirely and
then failed the seed on a missing column.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-28 14:03:30 +02:00

253 lines
10 KiB
SQL

-- name: ListSignals :many
SELECT sig.entity_id, se.slug, sig.kind, sig.severity, sig.state,
te.slug AS target_slug, sig.check_id, sig.evidence, sig.likely_cause,
sig.occurrence_count, sig.flap_count, sig.hold_down_until,
sig.mute_until, sig.first_seen_at, sig.last_seen_at
FROM signals sig
JOIN entities se ON se.id = sig.entity_id
LEFT JOIN entities te ON te.id = sig.target_entity_id
WHERE (sqlc.narg('state')::text IS NULL OR sig.state = sqlc.narg('state'))
AND (sqlc.narg('severity')::text IS NULL OR sig.severity = sqlc.narg('severity'))
AND (sqlc.narg('target')::text IS NULL OR te.slug = sqlc.narg('target'))
AND (sqlc.narg('kind')::text IS NULL OR sig.kind = sqlc.narg('kind'))
AND (sqlc.narg('cursor')::text IS NULL OR se.slug > sqlc.narg('cursor'))
ORDER BY se.slug
LIMIT sqlc.arg('lim');
-- name: GetIdempotentResponse :one
SELECT response_code, response_body, request_hash FROM idempotency_keys
WHERE actor = $1 AND key = $2;
-- name: PutIdempotentResponse :exec
INSERT INTO idempotency_keys (actor, key, request_hash, response_code, response_body)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (actor, key) DO NOTHING;
-- name: InsertAuditEntry :exec
INSERT INTO audit_log (actor_type, actor_id, action, entity_id, method, path,
status_code, detail, source_ip, correlation_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10);
-- name: InsertEvent :one
INSERT INTO events (type, entity_id, severity, source, data, correlation_id)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id, ts;
-- name: ListEvents :many
SELECT id, ts, type, entity_id, severity, source, data, correlation_id
FROM events
WHERE (sqlc.narg('type')::text IS NULL OR type = sqlc.narg('type'))
AND (sqlc.narg('entity_id')::uuid IS NULL OR entity_id = sqlc.narg('entity_id'))
AND (sqlc.narg('severity')::text IS NULL OR severity = sqlc.narg('severity'))
AND (sqlc.narg('correlation_id')::text IS NULL OR correlation_id = sqlc.narg('correlation_id'))
AND (sqlc.narg('from_ts')::timestamptz IS NULL OR ts >= sqlc.narg('from_ts'))
AND (sqlc.narg('to_ts')::timestamptz IS NULL OR ts <= sqlc.narg('to_ts'))
AND (sqlc.narg('before_id')::bigint IS NULL OR id < sqlc.narg('before_id'))
ORDER BY id DESC
LIMIT sqlc.arg('lim');
-- name: ListEventsAfter :many
SELECT id, ts, type, entity_id, severity, source, data, correlation_id
FROM events WHERE id > $1 ORDER BY id ASC LIMIT $2;
-- =====================================================================
-- Phase 3 queries
-- =====================================================================
-- name: ListEnabledCheckDefs :many
-- Enabled AND due. interval_s used to be selected but never filtered on, so
-- every check ran on every 30s pass and the declared intervals meant nothing.
-- NULL last_run_at = never run = due now.
SELECT cd.entity_id, cd.target_id, cd.target_type, cd.kind, cd.config,
cd.interval_s, cd.timeout_s, cd.zone, cd.enabled, cd.updated_at,
e.slug AS entity_slug
FROM check_defs cd
JOIN entities e ON e.id = cd.entity_id
WHERE cd.enabled = true
AND (cd.last_run_at IS NULL
OR cd.last_run_at <= now() - make_interval(secs => cd.interval_s));
-- name: MarkCheckRun :exec
UPDATE check_defs SET last_run_at = now() WHERE entity_id = $1;
-- name: GetCheckDef :one
SELECT * FROM check_defs WHERE entity_id = $1;
-- name: InsertCheckDef :exec
INSERT INTO check_defs (entity_id, target_id, target_type, kind, config, interval_s, timeout_s, zone, enabled)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9);
-- name: UpdateCheckDef :exec
UPDATE check_defs SET kind = $2, config = $3, interval_s = $4, timeout_s = $5,
target_id = $6, target_type = $7, zone = $8, enabled = $9, updated_at = now()
WHERE entity_id = $1;
-- name: UpsertSignal :one
INSERT INTO signals (entity_id, kind, severity, target_entity_id, check_id, evidence, likely_cause, state)
VALUES ($1, $2, $3, $4, $5, $6, $7, 'raised')
ON CONFLICT (target_entity_id, kind) WHERE state NOT IN ('resolved','failed')
DO UPDATE SET occurrence_count = signals.occurrence_count + 1,
last_seen_at = now(),
evidence = EXCLUDED.evidence,
updated_at = now()
RETURNING *;
-- name: GetOpenSignalsForAutoAct :many
-- Signals with auto-act classifications that haven't been executed yet
SELECT s.*, c.entity_id AS classification_id, c.action, c.risk_class, c.route,
c.blast_radius, c.correlation_id, c.reasoning
FROM classifications c
JOIN signals s ON s.entity_id = c.signal_entity_id
LEFT JOIN executions e ON e.classification_id = c.entity_id
WHERE c.route = 'auto-act'
AND e.entity_id IS NULL
AND (s.hold_down_until IS NULL OR s.hold_down_until < now())
AND (s.mute_until IS NULL OR s.mute_until < now())
ORDER BY s.last_seen_at ASC
LIMIT $1;
-- name: ListClassifications :many
SELECT c.entity_id, c.signal_entity_id, c.target_entity_id, c.action,
c.recommended_action, c.risk_class, c.route, c.blast_radius,
c.pattern_confidence, c.skill_id, c.autonomy_check, c.reasoning,
c.correlation_id, c.created_at,
e.slug AS target_slug
FROM classifications c
JOIN entities e ON e.id = c.target_entity_id
WHERE (sqlc.narg('route')::text IS NULL OR c.route = sqlc.narg('route'))
AND (sqlc.narg('cursor')::text IS NULL OR e.slug > sqlc.narg('cursor'))
ORDER BY e.slug
LIMIT sqlc.arg('lim');
-- name: InsertExecution :exec
INSERT INTO executions (entity_id, classification_id, signal_entity_id,
target_entity_id, action, risk_class, approval_id, agent_id,
skill_id, skill_version, status, correlation_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, 'proposed', $11);
-- name: UpdateExecutionStatus :exec
UPDATE executions SET status = $2, result = $3, duration_ms = $4,
verified = $5, started_at = COALESCE(started_at, now()),
completed_at = CASE WHEN $2 IN ('completed','failed','cancelled') THEN now() ELSE completed_at END
WHERE entity_id = $1;
-- name: GetExecution :one
SELECT * FROM executions WHERE entity_id = $1;
-- name: ListExecutions :many
SELECT e.entity_id, e.classification_id, e.signal_entity_id, e.target_entity_id,
e.action, e.risk_class, e.approval_id, e.agent_id,
e.skill_id, e.skill_version, e.status, e.result, e.duration_ms,
e.verified, e.correlation_id, e.started_at, e.completed_at, e.created_at,
te.slug AS target_slug
FROM executions e
JOIN entities te ON te.id = e.target_entity_id
WHERE (sqlc.narg('status')::text IS NULL OR e.status = sqlc.narg('status'))
AND (sqlc.narg('cursor')::text IS NULL OR te.slug > sqlc.narg('cursor'))
ORDER BY te.slug
LIMIT sqlc.arg('lim');
-- name: GetFeedbackAfterWatermark :many
SELECT f.entity_id, f.execution_id, f.outcome, f.observation, f.lesson,
f.unexpected_side_effects, f.tags, f.created_at,
e.action, e.risk_class, e.target_entity_id,
et.name AS applies_type
FROM feedback f
JOIN executions e ON e.entity_id = f.execution_id
JOIN entities ent ON ent.id = e.target_entity_id
JOIN entity_types et ON et.name = ent.type
WHERE f.created_at > $1
ORDER BY f.created_at ASC;
-- name: UpsertPattern :exec
INSERT INTO patterns (entity_id, applies_type, action, pattern, confidence,
evidence_count, success_count, failure_count, status, version)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'hypothesized', 1)
ON CONFLICT (applies_type, action)
DO UPDATE SET evidence_count = patterns.evidence_count + EXCLUDED.evidence_count,
success_count = patterns.success_count + EXCLUDED.success_count,
failure_count = patterns.failure_count + EXCLUDED.failure_count,
updated_at = now();
-- name: GetPattern :one
SELECT * FROM patterns WHERE applies_type = $1 AND action = $2;
-- name: ListPatterns :many
SELECT p.* FROM patterns p
WHERE (sqlc.narg('status')::text IS NULL OR p.status = sqlc.narg('status'))
ORDER BY p.applies_type, p.action;
-- name: UpdatePatternStatus :exec
UPDATE patterns SET status = $2, version = version + 1,
last_validated_at = CASE WHEN $2 = 'validated' THEN now() ELSE last_validated_at END
WHERE entity_id = $1;
-- name: UpdatePatternQuarantine :exec
UPDATE patterns SET quarantined = $2 WHERE entity_id = $1;
-- name: ListSkills :many
SELECT * FROM skills
WHERE (sqlc.narg('status')::text IS NULL OR status = sqlc.narg('status'))
ORDER BY name, version DESC;
-- name: UpdateSkillStatus :exec
UPDATE skills SET status = $2, last_used_at = now() WHERE entity_id = $1 AND version = $2;
-- name: InsertApproval :exec
INSERT INTO approvals (entity_id, subject_entity_id, action, risk_class, kind,
payload, status, token_hash, expires_at)
VALUES ($1, $2, $3, $4, $5, $6, 'pending', $7, $8);
-- name: ListApprovals :many
SELECT a.*, e.slug AS subject_slug
FROM approvals a
JOIN entities e ON e.id = a.subject_entity_id
WHERE (sqlc.narg('status')::text IS NULL OR a.status = sqlc.narg('status'))
AND (sqlc.narg('cursor')::text IS NULL OR e.slug > sqlc.narg('cursor'))
ORDER BY e.slug
LIMIT sqlc.arg('lim');
-- name: GetApprovalByID :one
SELECT * FROM approvals WHERE entity_id = $1;
-- name: UpdateApprovalStatus :exec
UPDATE approvals SET status = $2, decided_at = now(), decided_by = $3
WHERE entity_id = $1 AND status = 'pending';
-- name: GetAutonomySetting :one
SELECT value FROM autonomy_settings WHERE key = $1;
-- name: ListRiskClasses :many
SELECT * FROM risk_classes ORDER BY name;
-- name: ListApprovalRules :many
SELECT * FROM approval_rules ORDER BY entity_type, action;
-- name: InsertMetricSample :exec
INSERT INTO metric_samples (entity_id, metric, value, tags, ts)
VALUES ($1, $2, $3, $4, now());
-- name: QueryMetrics :many
SELECT time_bucket(sqlc.arg('bucket_interval')::interval, ts) AS bucket,
entity_id, metric,
ROUND(avg(value)::numeric, 2) AS avg_val,
ROUND(min(value)::numeric, 2) AS min_val,
ROUND(max(value)::numeric, 2) AS max_val
FROM metric_samples
WHERE entity_id = $1
AND metric = $2
AND ts > $3
GROUP BY bucket, entity_id, metric
ORDER BY bucket DESC;
-- name: UpsertEntityStatus :exec
INSERT INTO entity_status (entity_id, health, last_check_at, details)
VALUES ($1, $2, $3, $4)
ON CONFLICT (entity_id)
DO UPDATE SET health = EXCLUDED.health,
last_check_at = EXCLUDED.last_check_at,
details = EXCLUDED.details,
updated_at = now();
-- name: GetEntityStatus :one
SELECT * FROM entity_status WHERE entity_id = $1;