Let request controls outlive quote waits
All checks were successful
deploy / deploy (push) Successful in 33s

Proof: Live dashboard preflight waited through the generic 3s proxy timeout while trade-executor later recorded solver_quote_unanswered after the 10s relay quote wait. Request controls now use action-aware timeouts and unanswered requests render with plain reason text.

Assumptions: Own-request preflight needs at least quote_timeout plus small overhead; submit needs publish plus relay-status wait. Generic service refresh controls should keep the shorter dashboard upstream timeout.

Still fake: This does not create external solver liquidity; it only lets the dashboard observe whether the request was answered, submitted, or blocked without timing out first.
This commit is contained in:
philipp 2026-04-12 19:22:24 +02:00
parent 4d9347d55f
commit a4a60fd521
5 changed files with 88 additions and 1 deletions

View file

@ -15,6 +15,7 @@ import {
createDashboardLiveState, createDashboardLiveState,
listDashboardServices, listDashboardServices,
resolveDashboardControl, resolveDashboardControl,
resolveDashboardControlTimeoutMs,
} from '../core/operator-dashboard.mjs'; } from '../core/operator-dashboard.mjs';
import { import {
buildDashboardAuthChallengeHeader, buildDashboardAuthChallengeHeader,
@ -524,7 +525,7 @@ async function invokeControl(control, body) {
'content-type': 'application/json', 'content-type': 'application/json',
}, },
body: JSON.stringify(body || {}), body: JSON.stringify(body || {}),
signal: AbortSignal.timeout(config.operatorDashboardUpstreamTimeoutMs), signal: AbortSignal.timeout(resolveDashboardControlTimeoutMs({ control, config })),
}, },
); );

View file

@ -260,6 +260,27 @@ export function buildDashboardControlErrorResponse(error, { control = null } = {
}; };
} }
export function resolveDashboardControlTimeoutMs({ control, config } = {}) {
const baseTimeoutMs = Number(config?.operatorDashboardUpstreamTimeoutMs || 3_000);
if (control?.service !== 'trade-executor') return baseTimeoutMs;
if (control.action === 'intent-request-preflight') {
return Math.max(baseTimeoutMs, Number(config?.intentRequestQuoteTimeoutMs || 10_000) + 2_000);
}
if (control.action === 'intent-request-submit') {
return Math.max(
baseTimeoutMs,
Number(config?.intentRequestPublishTimeoutMs || 10_000)
+ Number(config?.intentRequestStatusTimeoutMs || 10_000)
+ 5_000,
);
}
if (control.action === 'intent-request-refresh-outcomes') {
return Math.max(baseTimeoutMs, Number(config?.intentRequestStatusTimeoutMs || 10_000) + 5_000);
}
return baseTimeoutMs;
}
export function listDashboardServices(config) { export function listDashboardServices(config) {
return SERVICE_DEFINITIONS.map(([service, label, configKey]) => ({ return SERVICE_DEFINITIONS.map(([service, label, configKey]) => ({
service, service,

View file

@ -1363,6 +1363,8 @@ function humanizeIntentRequestReason(reason) {
'Relay reported the intent as not found or not valid.', 'Relay reported the intent as not found or not valid.',
relay_settled_without_expected_inventory_delta: relay_settled_without_expected_inventory_delta:
'Relay reports settlement, but durable inventory does not show the expected EURe decrease and BTC increase.', 'Relay reports settlement, but durable inventory does not show the expected EURe decrease and BTC increase.',
solver_quote_unanswered:
'The relay returned no solver quotes for this request.',
}; };
return labels[normalized] || normalized.replaceAll('_', ' '); return labels[normalized] || normalized.replaceAll('_', ' ');
} }

View file

@ -10,6 +10,7 @@ import {
createDashboardLiveState, createDashboardLiveState,
deriveQuoteLifecycleRows, deriveQuoteLifecycleRows,
resolveDashboardControl, resolveDashboardControl,
resolveDashboardControlTimeoutMs,
} from '../src/core/operator-dashboard.mjs'; } from '../src/core/operator-dashboard.mjs';
import { import {
buildDashboardSessionToken, buildDashboardSessionToken,
@ -105,6 +106,31 @@ test('profitability summary flags cash-flow-adjusted benchmarks after later fund
assert.match(summary.caveats[0], /external cash flows/); assert.match(summary.caveats[0], /external cash flows/);
}); });
test('request creation controls get enough upstream time for relay quote and status waits', () => {
const preflight = resolveDashboardControl({
service: 'trade-executor',
action: 'intent-request-preflight',
});
const submit = resolveDashboardControl({
service: 'trade-executor',
action: 'intent-request-submit',
});
const refresh = resolveDashboardControl({
service: 'inventory-sync',
action: 'refresh',
});
const config = {
operatorDashboardUpstreamTimeoutMs: 3000,
intentRequestQuoteTimeoutMs: 10000,
intentRequestPublishTimeoutMs: 10000,
intentRequestStatusTimeoutMs: 10000,
};
assert.equal(resolveDashboardControlTimeoutMs({ control: preflight, config }), 12000);
assert.equal(resolveDashboardControlTimeoutMs({ control: submit, config }), 25000);
assert.equal(resolveDashboardControlTimeoutMs({ control: refresh, config }), 3000);
});
test('dashboard control errors become structured responses instead of uncaught failures', () => { test('dashboard control errors become structured responses instead of uncaught failures', () => {
const control = resolveDashboardControl({ const control = resolveDashboardControl({
service: 'trade-executor', service: 'trade-executor',

View file

@ -115,3 +115,40 @@ test('intent request status refresh loader normalizes accepted relay submissions
assert.equal(row.submitted_at, '2026-04-12T16:45:43.133Z'); assert.equal(row.submitted_at, '2026-04-12T16:45:43.133Z');
assert.equal(row.status_checked_at, '2026-04-12T16:45:44.000Z'); assert.equal(row.status_checked_at, '2026-04-12T16:45:44.000Z');
}); });
test('intent request normalization explains unanswered solver quotes in plain terms', () => {
const row = normalizeIntentRequestRow({
preflight_observed_at: '2026-04-12T17:20:19.476Z',
preflight_ingested_at: '2026-04-12T17:20:19.476Z',
preflight_payload: {
request_id: 'request-unanswered',
idempotency_key: 'intent-request:request-unanswered',
state: 'blocked',
reason_code: 'solver_quote_unanswered',
reason_text: 'The relay returned no solver quotes for this request.',
source_asset_id: 'nep141:eure.omft.near',
source_symbol: 'EURe',
source_decimals: 18,
destination_asset_id: 'nep141:btc.omft.near',
destination_symbol: 'BTC',
destination_decimals: 8,
source_amount_units: '5000000000000000000',
min_destination_amount_units: '8090',
solver_quote_count: 0,
created_at: '2026-04-12T17:20:19.476Z',
},
outcome_payload: {
request_id: 'request-unanswered',
outcome_status: 'blocked',
outcome_reason: 'solver_quote_unanswered',
attribution_status: 'unattributed',
attributed_inventory_delta: null,
},
});
assert.equal(row.state, 'blocked');
assert.equal(row.reason_code, 'solver_quote_unanswered');
assert.equal(row.reason_text, 'The relay returned no solver quotes for this request.');
assert.equal(row.live_submit_capable, false);
});