fix(onboarding): raise snapshot timeout + staged still-working UI (#2156) (#2179)

Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
This commit is contained in:
obchain
2026-05-19 21:08:59 -07:00
committed by GitHub
co-authored by Steven Enamakel
parent a40272ef45
commit 9376ffc19f
18 changed files with 662 additions and 25 deletions
+37 -8
View File
@@ -12,6 +12,27 @@ interface CoreRpcRelayRequest {
method: string;
params?: unknown;
serviceManaged?: boolean;
/**
* Per-call timeout override in milliseconds. When omitted, defaults to the
* global `CORE_RPC_TIMEOUT_MS` (30s). Use for slow-but-alive RPCs such as
* first-launch `openhuman.app_state_snapshot` (#2156). Clamped to the same
* [MIN, MAX] window as the global default.
*/
timeoutMs?: number;
}
/** Mirror of `parseCoreRpcTimeoutMs` bounds in `utils/config.ts`. */
const PER_CALL_TIMEOUT_MIN_MS = 1_000;
const PER_CALL_TIMEOUT_MAX_MS = 10 * 60 * 1_000;
function resolvePerCallTimeoutMs(override: number | undefined): number {
if (override === undefined) return CORE_RPC_TIMEOUT_MS;
if (!Number.isFinite(override)) return CORE_RPC_TIMEOUT_MS;
const clamped = Math.min(
Math.max(Math.round(override), PER_CALL_TIMEOUT_MIN_MS),
PER_CALL_TIMEOUT_MAX_MS
);
return clamped;
}
interface JsonRpcRequestBody {
@@ -344,7 +365,8 @@ export async function getCoreRpcToken(): Promise<string | null> {
*/
export async function testCoreRpcConnection(
url: string,
tokenOverride?: string
tokenOverride?: string,
init?: { signal?: AbortSignal }
): Promise<Response> {
const token = tokenOverride?.trim() || (await getCoreRpcToken());
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
@@ -355,6 +377,7 @@ export async function testCoreRpcConnection(
method: 'POST',
headers,
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'core.ping', params: {} }),
signal: init?.signal,
});
}
@@ -393,6 +416,7 @@ export async function callCoreRpc<T>({
method,
params,
serviceManaged = false, // kept for compatibility; direct frontend RPC does not use relay-level routing.
timeoutMs,
}: CoreRpcRelayRequest): Promise<T> {
void serviceManaged;
@@ -401,6 +425,7 @@ export async function callCoreRpc<T>({
}
const normalizedMethod = normalizeRpcMethod(method);
const effectiveTimeoutMs = resolvePerCallTimeoutMs(timeoutMs);
const payload: JsonRpcRequestBody = {
jsonrpc: '2.0',
id: nextJsonRpcId++,
@@ -419,12 +444,14 @@ export async function callCoreRpc<T>({
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
// Bound the fetch to CORE_RPC_TIMEOUT_MS. Without this a hung core
// sidecar will block every caller (and the UI) forever. We use a
// manual AbortController + setTimeout rather than AbortSignal.timeout()
// so test fake timers can drive the abort deterministically.
// Bound the fetch. Without this a hung core sidecar would block every
// caller (and the UI) forever. We use a manual AbortController +
// setTimeout rather than AbortSignal.timeout() so test fake timers can
// drive the abort deterministically. Per-call `timeoutMs` (clamped) lets
// legitimately-slow RPCs such as first-launch `app_state_snapshot`
// (#2156) opt into a longer-but-still-bounded budget.
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), CORE_RPC_TIMEOUT_MS);
const timeoutId = setTimeout(() => controller.abort(), effectiveTimeoutMs);
let response: Response;
try {
response = await fetch(rpcUrl, {
@@ -437,9 +464,11 @@ export async function callCoreRpc<T>({
if (controller.signal.aborted) {
// Throw a fully-classified `CoreRpcError` here so the outer catch
// doesn't re-wrap a bare `Error` and so callers can branch on
// `err.kind === 'timeout'` (Sentry filter, soft toast skip).
// `err.kind === 'timeout'` (Sentry filter, soft toast skip). Use
// the per-call `effectiveTimeoutMs` so the message reflects the
// actual budget (#2156 raised the snapshot path to 90s).
throw new CoreRpcError(
`Core RPC ${payload.method} timed out after ${CORE_RPC_TIMEOUT_MS}ms`,
`Core RPC ${payload.method} timed out after ${effectiveTimeoutMs}ms`,
'timeout'
);
}