fix(relay): handle dead connections and preserve usage on refresh failure

Use received frames for peer liveness and reject new requests after terminal relay failures. Keep authoritative usage snapshots separate from refresh errors, bound request lifetime, and guard concurrent refreshes and runtime switches.

Add regression coverage for relay liveness, terminal rejections, quota failures, cancellation and request deadlines. Validated with 51 focused tests, workspace type-check and lint, web build and mobile assets.
This commit is contained in:
Bohdan Triapitsyn
2026-09-09 17:41:14 +03:00
parent 0841ccdd57
commit cc25e58b55
23 changed files with 414 additions and 88 deletions
+12 -3
View File
@@ -332,6 +332,8 @@ export const MobileSessionMetadataButton = React.memo(function MobileSessionMeta
),
);
const quotaResults = useQuotaStore((state) => state.results);
const quotaRefreshErrors = useQuotaStore((state) => state.refreshErrors);
const quotaRefreshAttempted = React.useRef(false);
const loadQuotaSettings = useQuotaStore((state) => state.loadSettings);
const fetchAllQuotas = useQuotaStore((state) => state.fetchAllQuotas);
const isQuotaLoading = useQuotaStore((state) => state.isLoading);
@@ -350,13 +352,20 @@ export const MobileSessionMetadataButton = React.memo(function MobileSessionMeta
}, [dropdownProviderIds]);
React.useEffect(() => {
if (!open || isQuotaLoading) return;
if (!open) {
quotaRefreshAttempted.current = false;
return;
}
if (quotaRefreshAttempted.current || isQuotaLoading) return;
const missingEnabledProvider = dropdownProviderIds.some((providerId) => (
!quotaResults.some((result) => result.providerId === providerId)
!quotaResults.some((result) => result.providerId === providerId) || quotaRefreshErrors[providerId]
));
if (!missingEnabledProvider) return;
// Trigger at most one attempt per opening. A failed first load remains
// unknown, not an empty result that can suppress retries.
quotaRefreshAttempted.current = true;
void fetchAllQuotas();
}, [dropdownProviderIds, fetchAllQuotas, isQuotaLoading, open, quotaResults]);
}, [dropdownProviderIds, fetchAllQuotas, isQuotaLoading, open, quotaResults, quotaRefreshErrors]);
const latestMessageModel = React.useMemo(() => {
for (let i = activeSessionMessages.length - 1; i >= 0; i -= 1) {