Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2636,7 +2636,7 @@ jobs:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
repository: OmniNode-ai/omninode_infra
ref: 39033d55147ef22a061b665345b506246d3aa543
ref: db90c19241eb2b58779e18a1ddf7b9a0e11c9c7c
path: .proof-dependencies/omninode-infra
token: ${{ steps.app-token-proof.outputs.token }}
persist-credentials: false
Expand Down
179 changes: 179 additions & 0 deletions docker/migrations/forward/100_create_gateway_link_health.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
-- =============================================================================
-- MIGRATION: Create gateway_link_health table + gateway_link_health_status view
-- =============================================================================
-- Ticket: OMN-15570 (Gateway lift Phase 0, item G3)
-- Design source: docs/design/2026-08-08-gateway-node-architecture-lift.md
-- (commit 9422d30b1), §3 Phase 0 row G3; liveness thresholds cited from
-- node_bus_forwarder_effect/contract.yaml:59-63.
--
-- PURPOSE:
-- Latest-known-state projection, one row per tenant edge (tenant_id),
-- materialized from onex.evt.omnibase-infra.gateway-heartbeat.v1 events.
-- Consumed by node_gateway_link_health_projection_compute (COMPUTE,
-- extracts fields + emits the upsert intent) and persisted by
-- node_gateway_link_health_write_effect (EFFECT, executes the upsert).
-- Mirrors the pr_state (091_pr_state.sql) latest-known-state pattern.
--
-- ABSENCE-OF-PROGRESS DESIGN (the load-bearing property of this migration):
-- ON CONFLICT (tenant_id) DO UPDATE means a tenant edge that stops sending
-- heartbeats keeps its EXISTING row with a staling last_seen_at -- the row
-- is never deleted and never simply absent. gateway_link_health_status
-- (the view below) computes health_status live, at query time, by diffing
-- NOW() against last_seen_at against the contract's
-- max_silence_window_seconds threshold. This is deliberate: a stored,
-- write-time-computed health_status column would always read HEALTHY
-- (it is only written when a heartbeat DOES arrive), which cannot express
-- "went quiet" -- the exact alerting requirement ("alerting on ABSENCE of
-- progress ... never a missing row").
--
-- SCOPE DISCLOSURE (read before trusting lag_messages/lag_seconds):
-- The contract also declares lag_threshold_messages (500) and
-- lag_threshold_seconds (120), but no producer in this codebase publishes
-- lag data on the heartbeat topic today. G1 (OMN-15741, path-verifying
-- healthcheck) and G2 (OMN-15742, reconnect supervision) have both since
-- merged, but neither added lag telemetry: G2 widened
-- ModelGatewayHeartbeat with status/consecutive_failures/detail, not with
-- lag_messages/lag_seconds. lag_messages/lag_seconds are therefore always
-- NULL today, and gateway_link_health_status only evaluates the
-- silence-window threshold it can actually observe -- a NULL lag column
-- never contributes a false HEALTHY/UNHEALTHY verdict (NULL comparisons
-- are neither TRUE nor FALSE in SQL, so the lag CASE arms are inert until
-- a real producer populates these columns).
--
-- LIVENESS THRESHOLDS (node_bus_forwarder_effect/contract.yaml:59-63,
-- gateway_forwarder.liveness block):
-- max_silence_window_seconds: 60
-- lag_threshold_messages: 500
-- lag_threshold_seconds: 120
-- Hardcoded into the view rather than read from a config table: this is
-- the only tenant profile deployed today (single canary tenant, single
-- contract) and hardcoding the ratchet ties the view directly to the
-- cited contract lines for auditability. Revisit if/when multiple tenant
-- liveness profiles exist.
--
-- OMN-15762 (4th-copy class): this is the 4th independent copy of these
-- three numbers -- contract.yaml, ModelGatewayForwarderConfig defaults,
-- deploy YAML, and now this view -- with no load-time cross-check between
-- them. There is no existing repo pattern for materializing a value into
-- an already-applied SQL view at migration time (the runtime config
-- loader's _materialize_contract_* functions resolve at process start
-- against a live YAML file; a CREATE OR REPLACE VIEW body is baked in
-- once, at migration-apply time, and this table is already live). Rather
-- than leave the drift silent, tests/unit/db/test_migration_100.py pins
-- these three literals against the contract's declared values and fails
-- the suite the moment they diverge -- see OMN-15762 for the tracked
-- de-duplication fix (a single materialized-config source all 4 copies
-- read from).
--
-- IDEMPOTENCY:
-- - CREATE TABLE IF NOT EXISTS / CREATE INDEX IF NOT EXISTS are safe to
-- re-run.
-- - CREATE OR REPLACE VIEW is safe to re-run.
--
-- ROLLBACK:
-- docker/migrations/rollback/rollback_100_create_gateway_link_health.sql
-- =============================================================================

CREATE SCHEMA IF NOT EXISTS omninode_internal;

CREATE TABLE IF NOT EXISTS omninode_internal.gateway_link_health (
tenant_id TEXT NOT NULL,
principal_id TEXT NOT NULL,
local_transport_flavor TEXT NOT NULL,
last_seen_at TIMESTAMPTZ NOT NULL,
reported_status TEXT NOT NULL,
consecutive_failures INTEGER NOT NULL,
lag_messages BIGINT,
lag_seconds DOUBLE PRECISION,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (tenant_id)
);

CREATE INDEX IF NOT EXISTS idx_gateway_link_health_last_seen_at
ON omninode_internal.gateway_link_health (last_seen_at);

COMMENT ON TABLE omninode_internal.gateway_link_health IS
'Latest-known-state projection of gateway tenant-edge heartbeats '
'(OMN-15570, G3). One row per tenant_id, upserted on every heartbeat; '
'never deleted. Query gateway_link_health_status for live health, not '
'this table directly -- health is a function of NOW() - last_seen_at, '
'not a stored column.';

COMMENT ON COLUMN omninode_internal.gateway_link_health.last_seen_at IS
'Producer-supplied ModelGatewayHeartbeat.emitted_at -- the freshness '
'stamp gateway_link_health_status diffs against NOW().';

COMMENT ON COLUMN omninode_internal.gateway_link_health.reported_status IS
'The edge''s OWN verdict on itself, verbatim from '
'ModelGatewayHeartbeat.status (OMN-15742/G2). Stored as free TEXT, not a '
'CHECK-constrained enum, deliberately: gateway_link_health_status treats '
'any value other than ''active'' as degraded, so a status the producer '
'adds later (e.g. a drain state) is read as not-healthy rather than '
'crashing the projection or being silently scored HEALTHY.';

COMMENT ON COLUMN omninode_internal.gateway_link_health.consecutive_failures IS
'ModelGatewayHeartbeat.consecutive_failures -- the evidence behind a '
'degraded reported_status. Recorded so an operator can tell a single '
'blip from a sustained one without consulting process memory. It drives '
'no verdict arm of its own: the producer already folds it into '
'reported_status, and re-deriving it here would let the two disagree.';

COMMENT ON COLUMN omninode_internal.gateway_link_health.lag_messages IS
'Consumer lag in messages, when a producer supplies it. Always NULL '
'today -- see migration header SCOPE DISCLOSURE.';

COMMENT ON COLUMN omninode_internal.gateway_link_health.lag_seconds IS
'Consumer lag in seconds, when a producer supplies it. Always NULL '
'today -- see migration header SCOPE DISCLOSURE.';

-- =============================================================================
-- READ SURFACE: gateway_link_health_status
-- =============================================================================
-- Live per-tenant health, computed at query time. This is the surface
-- intended for a future omnidash widget (out of scope for OMN-15570 -- see
-- PR body) and for operator/alerting queries today, e.g.:
-- SELECT tenant_id, health_status, seconds_since_last_seen
-- FROM gateway_link_health_status
-- WHERE health_status != 'HEALTHY';
-- PRECEDENCE (fixed, deterministic -- the same row always yields the same
-- verdict, and exactly one arm can win because CASE stops at the first TRUE):
-- 1. UNHEALTHY -- stale beyond the silence window. Ranked
-- above the edge's self-report on purpose: if the edge has stopped
-- talking, its last self-report is stale too and cannot be trusted to
-- still describe reality.
-- 2. DEGRADED_SELF_REPORTED -- the edge is heartbeating on schedule and
-- says it is NOT well. Ranked above lag because it is a direct
-- first-party statement, not an inference from a derived metric.
-- 3. DEGRADED_LAG -- inferred from lag columns (inert today).
-- 4. HEALTHY -- fresh, self-reporting active, no lag breach.
CREATE OR REPLACE VIEW omninode_internal.gateway_link_health_status AS
SELECT
tenant_id,
principal_id,
local_transport_flavor,
last_seen_at,
reported_status,
consecutive_failures,
lag_messages,
lag_seconds,
updated_at,
EXTRACT(EPOCH FROM (NOW() - last_seen_at)) AS seconds_since_last_seen,
CASE
WHEN NOW() - last_seen_at > INTERVAL '60 seconds' THEN 'UNHEALTHY'
WHEN reported_status <> 'active' THEN 'DEGRADED_SELF_REPORTED'
WHEN lag_messages IS NOT NULL AND lag_messages > 500 THEN 'DEGRADED_LAG'
WHEN lag_seconds IS NOT NULL AND lag_seconds > 120 THEN 'DEGRADED_LAG'
ELSE 'HEALTHY'
END AS health_status
FROM omninode_internal.gateway_link_health;

COMMENT ON VIEW omninode_internal.gateway_link_health_status IS
'Read-time health evaluation over gateway_link_health. health_status '
'flips to UNHEALTHY once a row goes stale beyond '
'max_silence_window_seconds (60s, node_bus_forwarder_effect/contract.yaml:61) '
'without the row ever disappearing -- absence of progress is visible as '
'a stale row, not a missing one. An edge that IS heartbeating on time but '
'reports itself degraded scores DEGRADED_SELF_REPORTED, never HEALTHY. '
'Precedence is fixed (stale > self-reported > lag) and documented above '
'the view body. DEGRADED_LAG arms remain inert until a producer '
'populates lag_messages/lag_seconds.';
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-- OMN-15570: Rollback for 100_create_gateway_link_health.sql.
--
-- Reverses creation of the gateway_link_health_status view, the
-- gateway_link_health latest-known-state projection table, and its
-- last_seen_at index. Manual execution only -- never auto-applied (rollback/
-- is not mounted to docker-entrypoint-initdb.d).

DROP VIEW IF EXISTS omninode_internal.gateway_link_health_status;
DROP INDEX IF EXISTS omninode_internal.idx_gateway_link_health_last_seen_at;
DROP TABLE IF EXISTS omninode_internal.gateway_link_health;
6 changes: 3 additions & 3 deletions docker/migrations/schema_fingerprint.sha256
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Schema migration fingerprint for omnibase_infra (auto-generated)
# Regenerate: python scripts/check_schema_fingerprint.py stamp
# Verify: python scripts/check_schema_fingerprint.py verify
sha256:9a5a9e3f3ec61f88abfeba77a0108476b8848a7d24926b18ec0ce4c3602248d8
generated_at: 2026-08-10T23:12:56Z
migration_file_count: 84
sha256:7e8eafcb5a0e88c727c5b331cdb3e244793acc815400e534560e3d7d6e41637a
generated_at: 2026-08-22T16:54:32Z
migration_file_count: 85
4 changes: 2 additions & 2 deletions docker/runners/runner-image.lock.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"base_image_digest": "sha256:3ba65aa20f86a0fad9df2b2c259c613df006b2e6d0bfcc8a146afb8c525a9751",
"gh_version": "2.67.0",
"identity_digest": "00e1e1b5419d2c70d66e048c4cd235de",
"identity_digest": "0b730798ae1059f21e667eaf8e5653fa",
"image_version": 7,
"kubectl_version": "1.32.1",
"python_version": "3.12",
"runner_version": "2.336.0",
"shared_env_digest": "ab7726c96d67995001339ef7",
"shared_env_digest": "beeaf561515525f79488721a",
"shared_env_install_args": "--frozen --all-extras --all-groups --no-install-project",
"uv_version": "0.6.14"
}
Loading
Loading