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,
listDashboardServices,
resolveDashboardControl,
resolveDashboardControlTimeoutMs,
} from '../core/operator-dashboard.mjs';
import {
buildDashboardAuthChallengeHeader,
@ -524,7 +525,7 @@ async function invokeControl(control, body) {
'content-type': 'application/json',
},
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) {
return SERVICE_DEFINITIONS.map(([service, label, configKey]) => ({
service,

View file

@ -1363,6 +1363,8 @@ function humanizeIntentRequestReason(reason) {
'Relay reported the intent as not found or not valid.',
relay_settled_without_expected_inventory_delta:
'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('_', ' ');
}

View file

@ -10,6 +10,7 @@ import {
createDashboardLiveState,
deriveQuoteLifecycleRows,
resolveDashboardControl,
resolveDashboardControlTimeoutMs,
} from '../src/core/operator-dashboard.mjs';
import {
buildDashboardSessionToken,
@ -105,6 +106,31 @@ test('profitability summary flags cash-flow-adjusted benchmarks after later fund
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', () => {
const control = resolveDashboardControl({
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.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);
});