unrip/src/core/alert-engine.mjs
philipp 0b7e5e2e6c
All checks were successful
deploy / deploy (push) Successful in 26s
Implement runtime health sentinel and angry dashboard
Proof: Runtime health sentinel, alert routing, and anomaly detection for stale/disconnected quote truth, truthful dashboard severity, webhook notifications, and safe executor containment.

Assumptions: Existing control APIs remain the service-local truth surface; external notification stays as a generic webhook sink; executor disarm is an allowed non-fund-moving containment action; current dashboard/operator files in the worktree belong to this turn and are intended to ship together.

Still fake: No live external receiver is configured; webhook delivery is implemented but unverified end-to-end in production; cluster rollout still depends on deploying the new image; no automatic deployment restart path was added.
2026-04-08 19:35:07 +02:00

452 lines
13 KiB
JavaScript

const DEFAULT_RECENT_LIMIT = 50;
export function createAlertEngine({
activePair,
priceStaleMs,
inventoryStaleMs,
fundingCreditPendingMs,
fundingStuckMs,
evaluationIntervalMs,
recentTransitionLimit = DEFAULT_RECENT_LIMIT,
}) {
const state = {
latest_price: null,
latest_inventory: null,
latest_liquidity_action: null,
latest_trade_result: null,
funding_observations: {},
active_alerts: {},
runtime_alert_keys: new Set(),
recent_transitions: [],
last_evaluated_at: null,
};
return {
applyEvent(topic, payload, now = new Date().toISOString()) {
switch (topic) {
case 'ref.market_price':
state.latest_price = payload;
break;
case 'state.intent_inventory':
state.latest_inventory = payload;
break;
case 'ops.liquidity_action':
state.latest_liquidity_action = payload;
break;
case 'ops.funding_observation':
if (payload?.funding_observation_id) {
state.funding_observations[payload.funding_observation_id] = payload;
}
break;
case 'exec.trade_result':
state.latest_trade_result = payload;
break;
default:
break;
}
return evaluateAlerts({
state,
activePair,
priceStaleMs,
inventoryStaleMs,
fundingCreditPendingMs,
fundingStuckMs,
recentTransitionLimit,
now,
});
},
evaluate(now = new Date().toISOString()) {
return evaluateAlerts({
state,
activePair,
priceStaleMs,
inventoryStaleMs,
fundingCreditPendingMs,
fundingStuckMs,
recentTransitionLimit,
now,
});
},
applyRuntimeAlerts(desiredAlerts = [], now = new Date().toISOString()) {
return reconcileRuntimeAlertState({
state,
desiredAlerts,
now,
recentTransitionLimit,
});
},
getState(now = new Date().toISOString()) {
return summarizeState({
state,
evaluationIntervalMs,
now,
});
},
};
}
function evaluateAlerts({
state,
activePair,
priceStaleMs,
inventoryStaleMs,
fundingCreditPendingMs,
fundingStuckMs,
recentTransitionLimit,
now,
}) {
const desired = new Map();
const nowValue = timestampValue(now);
const priceAgeMs = ageMs(state.latest_price?.observed_at || state.latest_price?.ingested_at, nowValue);
if (priceAgeMs == null || priceAgeMs > priceStaleMs) {
desired.set(
buildAlertKey({
alertCode: 'reference_price_stale',
serviceScope: 'market-reference-ingest',
pair: activePair,
}),
{
alert_code: 'reference_price_stale',
severity: 'warning',
reason: priceAgeMs == null
? 'no reference price has been observed'
: `reference price age ${priceAgeMs}ms exceeds ${priceStaleMs}ms`,
service_scope: 'market-reference-ingest',
pair: activePair,
asset_id: null,
tx_hash: null,
details: {
last_price_at: state.latest_price?.observed_at || state.latest_price?.ingested_at || null,
age_ms: priceAgeMs,
stale_after_ms: priceStaleMs,
},
},
);
}
const inventoryAgeMs = ageMs(
state.latest_inventory?.synced_at || state.latest_inventory?.ingested_at,
nowValue,
);
if (inventoryAgeMs == null || inventoryAgeMs > inventoryStaleMs) {
desired.set(
buildAlertKey({
alertCode: 'inventory_snapshot_stale',
serviceScope: 'inventory-sync',
}),
{
alert_code: 'inventory_snapshot_stale',
severity: 'warning',
reason: inventoryAgeMs == null
? 'no inventory snapshot has been observed'
: `inventory snapshot age ${inventoryAgeMs}ms exceeds ${inventoryStaleMs}ms`,
service_scope: 'inventory-sync',
pair: null,
asset_id: null,
tx_hash: null,
details: {
last_inventory_at: state.latest_inventory?.synced_at || state.latest_inventory?.ingested_at || null,
age_ms: inventoryAgeMs,
stale_after_ms: inventoryStaleMs,
},
},
);
}
for (const observation of Object.values(state.funding_observations)) {
const observationAgeMs = ageMs(observation.last_seen_at, nowValue);
const baseDetails = {
funding_observation_id: observation.funding_observation_id,
funding_handle: observation.funding_handle,
confirmations: observation.confirmations,
amount: observation.amount,
observation_status: observation.status,
first_seen_at: observation.first_seen_at,
last_seen_at: observation.last_seen_at,
age_ms: observationAgeMs,
bridge_deposit_tx_hash: observation.bridge_deposit_tx_hash,
bridge_status: observation.bridge_status,
};
if (observation.status === 'SEEN_UNCONFIRMED') {
desired.set(
buildAlertKey({
alertCode: 'funding_seen_unconfirmed',
serviceScope: 'liquidity-manager',
assetId: observation.asset_id,
txHash: observation.tx_hash,
}),
{
alert_code: 'funding_seen_unconfirmed',
severity: 'info',
reason: `funding tx ${observation.tx_hash} is visible before confirmations`,
service_scope: 'liquidity-manager',
pair: null,
asset_id: observation.asset_id,
tx_hash: observation.tx_hash,
details: baseDetails,
},
);
}
if (
observation.status === 'CREDIT_PENDING'
|| (observation.status === 'SEEN_CONFIRMED' && observationAgeMs != null && observationAgeMs >= fundingCreditPendingMs)
) {
desired.set(
buildAlertKey({
alertCode: 'funding_confirmed_credit_pending',
serviceScope: 'liquidity-manager',
assetId: observation.asset_id,
txHash: observation.tx_hash,
}),
{
alert_code: 'funding_confirmed_credit_pending',
severity: 'warning',
reason: `funding tx ${observation.tx_hash} is confirmed but not spendable yet`,
service_scope: 'liquidity-manager',
pair: null,
asset_id: observation.asset_id,
tx_hash: observation.tx_hash,
details: {
...baseDetails,
credit_pending_after_ms: fundingCreditPendingMs,
},
},
);
}
if (
observation.status === 'FAILED_OR_STUCK'
|| (
observation.status !== 'CREDITED'
&& observation.status !== 'SEEN_UNCONFIRMED'
&& observationAgeMs != null
&& fundingStuckMs != null
&& observationAgeMs >= fundingStuckMs
)
) {
desired.set(
buildAlertKey({
alertCode: 'funding_stuck',
serviceScope: 'liquidity-manager',
assetId: observation.asset_id,
txHash: observation.tx_hash,
}),
{
alert_code: 'funding_stuck',
severity: 'critical',
reason: `funding tx ${observation.tx_hash} is failed or stuck before credit`,
service_scope: 'liquidity-manager',
pair: null,
asset_id: observation.asset_id,
tx_hash: observation.tx_hash,
details: baseDetails,
},
);
}
}
const latestTradeResult = state.latest_trade_result;
if (latestTradeResult && isSubmissionFailure(latestTradeResult)) {
desired.set(
buildAlertKey({
alertCode: 'executor_submission_failed',
serviceScope: 'trade-executor',
pair: latestTradeResult.pair || activePair,
}),
{
alert_code: 'executor_submission_failed',
severity: 'critical',
reason: `executor submission failed for command ${latestTradeResult.command_id}`,
service_scope: 'trade-executor',
pair: latestTradeResult.pair || activePair,
asset_id: null,
tx_hash: null,
details: {
command_id: latestTradeResult.command_id,
quote_id: latestTradeResult.quote_id,
result_code: latestTradeResult.result_code,
error: latestTradeResult.error || null,
status: latestTradeResult.status,
},
},
);
}
const transitions = reconcileAlertState({
state,
desired,
now,
recentTransitionLimit,
});
state.last_evaluated_at = now;
return transitions;
}
function reconcileAlertState({ state, desired, now, recentTransitionLimit }) {
const transitions = [];
for (const [key, next] of desired.entries()) {
const existing = state.active_alerts[key];
if (!existing) {
const raised = {
...next,
status: 'raised',
first_raised_at: now,
raised_at: now,
cleared_at: null,
last_evaluated_at: now,
};
state.active_alerts[key] = raised;
transitions.push(raised);
continue;
}
state.active_alerts[key] = {
...existing,
...next,
status: 'raised',
raised_at: existing.raised_at || existing.first_raised_at || now,
first_raised_at: existing.first_raised_at || existing.raised_at || now,
cleared_at: null,
last_evaluated_at: now,
};
}
for (const [key, existing] of Object.entries(state.active_alerts)) {
if (desired.has(key)) continue;
const cleared = {
...existing,
status: 'cleared',
raised_at: existing.raised_at || existing.first_raised_at || now,
cleared_at: now,
last_evaluated_at: now,
};
delete state.active_alerts[key];
transitions.push(cleared);
}
if (transitions.length > 0) {
state.recent_transitions.unshift(...transitions);
state.recent_transitions = state.recent_transitions.slice(0, recentTransitionLimit);
}
return transitions;
}
function reconcileRuntimeAlertState({
state,
desiredAlerts,
now,
recentTransitionLimit,
}) {
const transitions = [];
const desired = new Map();
const nextRuntimeKeys = new Set();
for (const alert of desiredAlerts || []) {
const key = buildAlertKey({
alertCode: alert.alert_code,
serviceScope: alert.service_scope,
pair: alert.pair,
assetId: alert.asset_id,
txHash: alert.tx_hash,
});
desired.set(key, alert);
nextRuntimeKeys.add(key);
}
for (const key of state.runtime_alert_keys) {
if (desired.has(key)) continue;
const existing = state.active_alerts[key];
if (!existing) continue;
const cleared = {
...existing,
status: 'cleared',
raised_at: existing.raised_at || existing.first_raised_at || now,
cleared_at: now,
last_evaluated_at: now,
};
delete state.active_alerts[key];
transitions.push(cleared);
}
for (const [key, alert] of desired.entries()) {
const existing = state.active_alerts[key];
if (!existing) {
const raised = {
...alert,
status: 'raised',
first_raised_at: now,
raised_at: now,
cleared_at: null,
last_evaluated_at: now,
};
state.active_alerts[key] = raised;
transitions.push(raised);
continue;
}
state.active_alerts[key] = {
...existing,
...alert,
status: 'raised',
raised_at: existing.raised_at || existing.first_raised_at || now,
first_raised_at: existing.first_raised_at || existing.raised_at || now,
cleared_at: null,
last_evaluated_at: now,
};
}
state.runtime_alert_keys = nextRuntimeKeys;
if (transitions.length > 0) {
state.recent_transitions.unshift(...transitions);
state.recent_transitions = state.recent_transitions.slice(0, recentTransitionLimit);
}
state.last_evaluated_at = now;
return transitions;
}
function summarizeState({ state, evaluationIntervalMs, now }) {
const activeAlerts = Object.values(state.active_alerts)
.sort((left, right) => timestampValue(right.first_raised_at) - timestampValue(left.first_raised_at));
const nowValue = timestampValue(now);
return {
active_alerts: activeAlerts,
recent_transitions: state.recent_transitions,
last_evaluated_at: state.last_evaluated_at,
stale: ageMs(state.last_evaluated_at, nowValue) > (evaluationIntervalMs * 2),
latest_inputs: {
market_price_at: state.latest_price?.observed_at || state.latest_price?.ingested_at || null,
inventory_at: state.latest_inventory?.synced_at || state.latest_inventory?.ingested_at || null,
liquidity_action_at: state.latest_liquidity_action?.observed_at || null,
trade_result_at: state.latest_trade_result?.ingested_at || null,
funding_observation_count: Object.keys(state.funding_observations).length,
},
};
}
function buildAlertKey({ alertCode, serviceScope, pair = null, assetId = null, txHash = null }) {
return [alertCode, serviceScope, pair || '', assetId || '', txHash || ''].join('|');
}
function isSubmissionFailure(result) {
return result?.status === 'failed' || result?.result_code === 'submission_failed';
}
function ageMs(value, nowValue) {
const start = timestampValue(value);
if (!Number.isFinite(start) || !Number.isFinite(nowValue)) return null;
return Math.max(0, nowValue - start);
}
function timestampValue(value) {
if (!value) return NaN;
const parsed = Date.parse(value);
return Number.isFinite(parsed) ? parsed : NaN;
}