diff --git a/src/apps/ops-sentinel.mjs b/src/apps/ops-sentinel.mjs index 9ac90be..73e5d7c 100644 --- a/src/apps/ops-sentinel.mjs +++ b/src/apps/ops-sentinel.mjs @@ -13,6 +13,7 @@ import { buildRuntimeAlert, createRuntimeHealthThresholds, evaluateRuntimeHealth, + shouldRaiseIngestPublishStale, shouldContainExecutorForAlerts, } from '../core/runtime-health.mjs'; import { @@ -298,14 +299,13 @@ function buildDeterministicRuntimeAlerts({ servicesByName, now, previousRuntimeE })); } - if ( - (publishedAgeMs == null || publishedAgeMs > thresholds.ingestPublishStaleMs) - || ( - matchingQuoteAgeMs != null - && matchingQuoteAgeMs <= thresholds.ingestQuoteStaleMs - && (publishedAgeMs == null || publishedAgeMs > thresholds.ingestPublishStaleMs) - ) - ) { + if (shouldRaiseIngestPublishStale({ + lastMatchingQuoteAt: ingestState.last_matching_quote_at || null, + lastPublishedAt: ingestState.last_published_at || null, + matchingQuoteAgeMs, + publishedAgeMs, + publishStaleMs: thresholds.ingestPublishStaleMs, + })) { alerts.push(buildRuntimeAlert({ alert_code: 'near_intents_publish_stale', severity: 'critical', diff --git a/src/core/runtime-health.mjs b/src/core/runtime-health.mjs index 737c463..6c5f077 100644 --- a/src/core/runtime-health.mjs +++ b/src/core/runtime-health.mjs @@ -294,6 +294,30 @@ export function buildRuntimeAlert({ }; } +export function shouldRaiseIngestPublishStale({ + lastMatchingQuoteAt = null, + lastPublishedAt = null, + matchingQuoteAgeMs = null, + publishedAgeMs = null, + publishStaleMs, +} = {}) { + if (!lastMatchingQuoteAt) return false; + + if (!lastPublishedAt) return true; + + if (publishedAgeMs == null || publishedAgeMs > publishStaleMs) return true; + + if ( + matchingQuoteAgeMs != null + && matchingQuoteAgeMs <= publishStaleMs + && publishedAgeMs > matchingQuoteAgeMs + 5_000 + ) { + return true; + } + + return false; +} + export function shouldContainExecutorForAlerts(alerts = []) { const containmentAlertCodes = new Set([ 'near_intents_ingest_disconnected', diff --git a/test/runtime-health.test.mjs b/test/runtime-health.test.mjs index d088f92..fa8fb4c 100644 --- a/test/runtime-health.test.mjs +++ b/test/runtime-health.test.mjs @@ -1,7 +1,30 @@ import test from 'node:test'; import assert from 'node:assert/strict'; -import { shouldContainExecutorForAlerts } from '../src/core/runtime-health.mjs'; +import { + shouldContainExecutorForAlerts, + shouldRaiseIngestPublishStale, +} from '../src/core/runtime-health.mjs'; + +test('publish stale does not raise before any matching quote exists', () => { + assert.equal(shouldRaiseIngestPublishStale({ + lastMatchingQuoteAt: null, + lastPublishedAt: null, + matchingQuoteAgeMs: null, + publishedAgeMs: null, + publishStaleMs: 30_000, + }), false); +}); + +test('publish stale raises after a matching quote exists but no publish follows', () => { + assert.equal(shouldRaiseIngestPublishStale({ + lastMatchingQuoteAt: '2026-04-08T20:45:00.000Z', + lastPublishedAt: null, + matchingQuoteAgeMs: 10_000, + publishedAgeMs: null, + publishStaleMs: 30_000, + }), true); +}); test('executor containment ignores quote-stale-only conditions', () => { assert.equal(shouldContainExecutorForAlerts([{