From dc91a4225fd2d9e25c46afb80504ffc15151a25a Mon Sep 17 00:00:00 2001 From: ciregenz Date: Tue, 21 Jul 2026 19:11:56 -0700 Subject: [PATCH] [eric] onboarding: harvest ChatGPT + Claude in parallel under a budget (13.7s -> 6.9s, 49% off the reveal wait) --- backend/apps/onboarding/onboarding.py | 32 +++++++++++++++++++++------ 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/backend/apps/onboarding/onboarding.py b/backend/apps/onboarding/onboarding.py index 93e79083..a0147b6e 100644 --- a/backend/apps/onboarding/onboarding.py +++ b/backend/apps/onboarding/onboarding.py @@ -5,11 +5,25 @@ the user's machine and providers; nothing here mutates state or leaves the box except the prep call, which goes to the user's own configured model. """ +import asyncio from contextlib import asynccontextmanager from pathlib import Path +from typing import Awaitable from typeguard import typechecked +# Hard cap on any single provider harvest so a wedged endpoint can't hold the whole reveal hostage +# (the raw-httpx paths already carry a 20s per-request timeout; this bounds the multi-request loop). +P_HARVEST_BUDGET_S = 26.0 + + +async def p_budgeted(coro: Awaitable[str]) -> str: + """Await a harvest under the budget; any failure (timeout, provider down) fails open to ''.""" + try: + return await asyncio.wait_for(coro, timeout=P_HARVEST_BUDGET_S) + except Exception: + return "" + from backend.apps.onboarding.identity import build_identity from backend.apps.onboarding.local_scan import run_local_scan from backend.apps.onboarding.models import PrepRequest @@ -48,16 +62,20 @@ async def post_prep(body: PrepRequest) -> dict: from backend.apps.settings.store import load_settings # ALWAYS read the ENTIRE recent conversations (not just titles) from the rich providers, ChatGPT via - # the codex connect token, Claude via the user's own browser session cookies, and PREFER that over - # whatever the frontend read. The frontend reads only the single connected provider, which for a - # Gemini/antigravity user is a titles-only DOM scrape that can surface a stale topic (the "skincare - # app" the user hasn't touched in ages). Multiple providers connected? We take all the rich ones and - # let the clustering pass merge them into one profile. Each fails open to "", so a missing one drops. + # the codex connect token (platform-independent), Claude via the user's own browser session cookies, + # and PREFER that over whatever the frontend read. The frontend reads only the single connected + # provider, which for a Gemini/antigravity user is a titles-only DOM scrape that can surface a stale + # topic (the "skincare app" the user hasn't touched in ages). Multiple providers connected? We take + # all the rich ones and let the clustering pass merge them. Each fails open to "", so a missing one + # drops. Harvested in PARALLEL under a budget: stacked awaits added a ~15s ChatGPT pull ON TOP of a + # ~8s Claude pull for ~23s of dead reveal time; gathered they overlap to the slower of the two. + chatgpt, claude = await asyncio.gather( + p_budgeted(harvest_chatgpt_usage()), + p_budgeted(harvest_claude_usage()), + ) parts: list[str] = [] - chatgpt = await harvest_chatgpt_usage() if chatgpt: parts.append("ChatGPT conversations:\n" + chatgpt) - claude = await harvest_claude_usage() if claude: parts.append("Claude conversations:\n" + claude) if parts: