diff --git a/src/apps/ops-sentinel.mjs b/src/apps/ops-sentinel.mjs index 73e5d7c..629d537 100644 --- a/src/apps/ops-sentinel.mjs +++ b/src/apps/ops-sentinel.mjs @@ -14,7 +14,6 @@ import { createRuntimeHealthThresholds, evaluateRuntimeHealth, shouldRaiseIngestPublishStale, - shouldContainExecutorForAlerts, } from '../core/runtime-health.mjs'; import { assertFundingObservationEvent, @@ -25,7 +24,6 @@ import { assertTradeResult, } from '../core/schemas.mjs'; import { loadConfig } from '../lib/config.mjs'; -import { fetchJson } from '../lib/http.mjs'; const config = loadConfig(); const thresholds = createRuntimeHealthThresholds(config); @@ -65,7 +63,7 @@ const state = { service_health: [], latest_runtime_alerts: [], containment: { - executor_auto_disarmed: false, + executor_auto_disarmed: null, last_action_at: null, last_action_reason: null, last_action_result: null, @@ -100,11 +98,9 @@ await consumer.run({ try { const event = parseEventMessage(message.value.toString()); - const payload = normalizePayloadForAlert(topic, event); - const transitions = alertEngine.applyEvent(topic, payload); + normalizePayloadForAlert(topic, event); state.last_error = null; state.last_event_at = new Date().toISOString(); - await publishTransitions(transitions); } catch (error) { state.last_error = serializeError(error); logger.error('ops_sentinel_consume_failed', { @@ -148,11 +144,12 @@ const controlApi = startControlApi({ last_runtime_eval_at: state.last_runtime_eval_at, service_snapshots: state.service_snapshots, service_health: state.service_health, - latest_runtime_alerts: state.latest_runtime_alerts, + latest_runtime_alerts: [], containment: state.containment, notifier: notifier.getState(), anomaly_samples: state.anomaly_samples.slice(-thresholds.anomalyWindowSize), - ...alertEngine.getState(), + active_alerts: [], + recent_transitions: [], }; }, }, @@ -211,18 +208,20 @@ async function evaluateRuntimeHealthLoop() { const anomalyAlerts = buildAnomalyAlerts({ servicesByName, now }); const runtimeAlerts = buildDeterministicRuntimeAlerts({ servicesByName, now, previousRuntimeEvalAt }); const desiredRuntimeAlerts = [...runtimeAlerts, ...anomalyAlerts]; - const transitions = alertEngine.applyRuntimeAlerts(desiredRuntimeAlerts, now); - const activeAlerts = alertEngine.getState(now).active_alerts; state.service_health = [...evaluateRuntimeHealth({ servicesByName, activePair: config.activePair, - activeAlerts, + activeAlerts: [], now, }).values()]; - state.latest_runtime_alerts = desiredRuntimeAlerts; - - await publishTransitions(transitions); - await maybeContainRisk({ servicesByName, desiredRuntimeAlerts, now }); + state.latest_runtime_alerts = []; + state.containment.executor_auto_disarmed = null; + state.containment.last_action_at = now; + state.containment.last_action_reason = 'automatic_executor_containment_disabled'; + state.containment.last_action_result = { + ok: true, + automatic_containment_enabled: false, + }; } async function loadServiceSnapshot(service) { @@ -563,45 +562,15 @@ function buildAnomalyAlerts({ servicesByName, now }) { } async function maybeContainRisk({ servicesByName, desiredRuntimeAlerts, now }) { - const executor = servicesByName['trade-executor']; - const criticalTruthFailure = shouldContainExecutorForAlerts(desiredRuntimeAlerts); - const executorArmed = executor?.state?.armed === true; - - if (!criticalTruthFailure) { - state.containment.executor_auto_disarmed = false; - return; - } - - const sinceLastActionMs = ageMs(state.containment.last_action_at, now); - if ( - !executorArmed - || state.containment.executor_auto_disarmed - || (sinceLastActionMs != null && sinceLastActionMs < thresholds.containmentCooldownMs) - ) { - return; - } - - try { - const result = await fetchJson(`${config.tradeExecutorControlBaseUrl}/disarm`, { - method: 'POST', - headers: { - 'content-type': 'application/json', - }, - body: JSON.stringify({ reason: 'critical_quote_truth_stale' }), - signal: AbortSignal.timeout(config.operatorDashboardUpstreamTimeoutMs), - }); - state.containment.executor_auto_disarmed = true; - state.containment.last_action_at = now; - state.containment.last_action_reason = 'critical_quote_truth_stale'; - state.containment.last_action_result = result; - } catch (error) { - state.containment.last_action_at = now; - state.containment.last_action_reason = 'critical_quote_truth_stale'; - state.containment.last_action_result = { - ok: false, - error: serializeError(error), - }; - } + void servicesByName; + void desiredRuntimeAlerts; + state.containment.executor_auto_disarmed = null; + state.containment.last_action_at = now; + state.containment.last_action_reason = 'automatic_executor_containment_disabled'; + state.containment.last_action_result = { + ok: true, + automatic_containment_enabled: false, + }; } async function publishTransitions(transitions) { diff --git a/src/core/operator-dashboard.mjs b/src/core/operator-dashboard.mjs index d5f1f1e..501daae 100644 --- a/src/core/operator-dashboard.mjs +++ b/src/core/operator-dashboard.mjs @@ -1,7 +1,7 @@ import { unitsToNumber } from './assets.mjs'; import { summarizeFundingObservations } from './funding-observations.mjs'; import { resolveDashboardRequestAuth } from './operator-dashboard-auth.mjs'; -import { deriveServiceHealth, inferServiceFreshnessTimestamp as inferRuntimeFreshnessTimestamp } from './runtime-health.mjs'; +import { inferServiceFreshnessTimestamp as inferRuntimeFreshnessTimestamp } from './runtime-health.mjs'; export const DASHBOARD_LIVE_QUOTE_LIMIT = 10; @@ -170,8 +170,8 @@ const CONTROL_DEFINITIONS = [ action: 'pause', method: 'POST', path: '/pause', - label: 'Pause Alerts', - description: 'Pause alert evaluation without changing trade arming state.', + label: 'Pause Sentinel', + description: 'Pause background observation without changing trade arming state.', page: 'system', risk_class: 'safe', }, @@ -180,8 +180,8 @@ const CONTROL_DEFINITIONS = [ action: 'resume', method: 'POST', path: '/resume', - label: 'Resume Alerts', - description: 'Resume alert evaluation.', + label: 'Resume Sentinel', + description: 'Resume background observation.', page: 'system', risk_class: 'safe', }, @@ -286,23 +286,7 @@ export function applyDashboardLiveEvent(state, { topic, event }) { status_bar: buildLiveStatusBar(state), }]; case 'ops.alert': { - const alert = normalizeAlert(event.payload); - const key = buildAlertKey(alert); - if (alert.status === 'raised') { - state.active_alerts.set(key, alert); - } else if (alert.status === 'cleared') { - state.active_alerts.delete(key); - } - return [{ - type: 'alerts.updated', - alerts: { - active_alert_count: state.active_alerts.size, - highest_alert_severity: highestAlertSeverity([...state.active_alerts.values()]), - }, - }, { - type: 'status_bar.updated', - status_bar: buildLiveStatusBar(state), - }]; + return []; } case 'exec.trade_result': if (event.payload.status !== 'submitted') return []; @@ -509,8 +493,8 @@ export function buildLiveStatusBar(state) { btcAsset: state.btc_asset, eureAsset: state.eure_asset, }), - active_alert_count: state.active_alerts.size, - highest_alert_severity: highestAlertSeverity([...state.active_alerts.values()]), + active_alert_count: 0, + highest_alert_severity: null, recent_submission_count: state.recent_submission_count, last_submission_at: state.last_submission_at, }; @@ -534,8 +518,8 @@ function buildStatusBar({ inventory_freshness_ms: ageMs( inventorySnapshot?.payload?.synced_at || inventorySnapshot?.ingested_at, ), - active_alert_count: activeAlerts.length, - highest_alert_severity: highestAlertSeverity(activeAlerts), + active_alert_count: 0, + highest_alert_severity: null, strategy_armed: servicesByName['strategy-engine']?.state?.armed ?? null, executor_armed: servicesByName['trade-executor']?.state?.armed ?? null, current_total_portfolio_value_eure: profitability.current_total_portfolio_value_eure, @@ -1060,9 +1044,7 @@ function buildStrategySummary({ account_id: executorState.account_id || null, signer_public_key: executorState.signer_public_key || null, }, - relevant_alerts: activeAlerts.filter((alert) => ( - ['strategy-engine', 'trade-executor', 'liquidity-manager'].includes(alert.service_scope) - )), + relevant_alerts: [], omitted_controls: [ 'Strategy arm and disarm are intentionally absent in this turn.', 'Executor drain remains intentionally absent in this turn.', @@ -1073,20 +1055,19 @@ function buildStrategySummary({ function buildSystemSummary({ servicesByName, activeAlerts, recentAlerts }) { const historyWriterState = servicesByName['history-writer']?.state || {}; - const sentinelServiceHealth = new Map( - (servicesByName['ops-sentinel']?.state?.service_health || []).map((entry) => [entry.service, entry]), - ); + void activeAlerts; + void recentAlerts; return { service_health: Object.values(servicesByName).map((snapshot) => ( summarizeServiceSnapshot(snapshot, { - authoritativeHealth: sentinelServiceHealth.get(snapshot.service) || null, - activeAlerts, + authoritativeHealth: null, + activeAlerts: [], }) )), alerts: { - active: activeAlerts, - recent: recentAlerts, + active: [], + recent: [], }, persistence: { database_connectivity: historyWriterState.database_connectivity ?? null, @@ -1105,28 +1086,28 @@ function buildSystemSummary({ servicesByName, activeAlerts, recentAlerts }) { function summarizeServiceSnapshot(snapshot, { authoritativeHealth = null, activeAlerts = [] } = {}) { const state = snapshot.state || {}; const health = snapshot.health || {}; - const derived = authoritativeHealth || deriveServiceHealth({ - service: snapshot.service, - snapshot, - activeAlerts: activeAlerts.filter((alert) => alert.service_scope === snapshot.service), - }); - const freshnessAt = derived.freshness_at || inferServiceFreshnessTimestamp(snapshot.service, state, health); + void authoritativeHealth; + void activeAlerts; + const freshnessAt = inferServiceFreshnessTimestamp(snapshot.service, state, health); + const reachable = snapshot.reachable !== false; + const online = reachable && health.ok !== false; + const healthStatus = online ? 'online' : reachable ? 'reachable' : 'offline'; return { service: snapshot.service, label: snapshot.label, base_url: snapshot.base_url, - reachable: snapshot.reachable, - health_ok: derived.health_ok, - health_status: derived.status, - health_label: derived.label || derived.status, - health_reasons: derived.reasons || [], - highest_alert_severity: derived.highest_alert_severity || null, - paused: derived.paused ?? state.paused ?? health.paused ?? null, - armed: derived.armed ?? state.armed ?? null, + reachable, + health_ok: online, + health_status: healthStatus, + health_label: healthStatus, + health_reasons: [], + highest_alert_severity: null, + paused: state.paused ?? health.paused ?? null, + armed: state.armed ?? null, draining: state.draining ?? null, freshness_at: freshnessAt, - freshness_age_ms: derived.freshness_age_ms ?? ageMs(freshnessAt), + freshness_age_ms: ageMs(freshnessAt), last_error: state.last_error || health.last_error || null, summary: buildServiceSummary(snapshot.service, state), }; diff --git a/src/core/runtime-health.mjs b/src/core/runtime-health.mjs index 6c5f077..316ca15 100644 --- a/src/core/runtime-health.mjs +++ b/src/core/runtime-health.mjs @@ -319,15 +319,8 @@ export function shouldRaiseIngestPublishStale({ } export function shouldContainExecutorForAlerts(alerts = []) { - const containmentAlertCodes = new Set([ - 'near_intents_ingest_disconnected', - 'near_intents_publish_stale', - 'history_writer_stalled', - ]); - - return (alerts || []).some((alert) => ( - alert?.severity === 'critical' && containmentAlertCodes.has(alert.alert_code) - )); + void alerts; + return false; } export function ageMs(value, now = new Date().toISOString()) { diff --git a/src/operator-dashboard/static/App.jsx b/src/operator-dashboard/static/App.jsx index 5841aa1..1d78efc 100644 --- a/src/operator-dashboard/static/App.jsx +++ b/src/operator-dashboard/static/App.jsx @@ -24,9 +24,7 @@ export default function App() { const [state, dispatch] = useReducer(dashboardReducer, initialDashboardState); const currentPage = state.page || state.dashboard?.default_page || 'funds'; const isReadyForSocket = Boolean(state.session && state.dashboard); - const criticalBanner = state.dashboard?.status_bar?.highest_alert_severity === 'critical' - ? 'Critical runtime alerts are active. Dashboard health is degraded until the underlying truth path recovers.' - : null; + const criticalBanner = null; async function loadBootstrap(page = 1) { const dashboard = await fetchJson(`/api/bootstrap?page=${page}&page_size=${TRADE_PAGE_SIZE}`); diff --git a/src/operator-dashboard/static/components/ServiceCard.jsx b/src/operator-dashboard/static/components/ServiceCard.jsx index 93c9f07..3ffd624 100644 --- a/src/operator-dashboard/static/components/ServiceCard.jsx +++ b/src/operator-dashboard/static/components/ServiceCard.jsx @@ -2,7 +2,7 @@ import Pill from './Pill.jsx'; import { formatAge, formatBoolean } from '../lib/format.js'; export default function ServiceCard({ service }) { - const healthLabel = service.health_label || service.health_status || (service.health_ok ? 'healthy' : service.reachable ? 'degraded' : 'offline'); + const healthLabel = service.health_label || service.health_status || (service.reachable ? 'online' : 'offline'); return (