{"record":{"id":"2f6c722012d4b3cf","repo":"unslothai/unsloth","slug":"deadline-reached-before-method-redact-url-url","errorCode":null,"errorMessage":"deadline reached before {method} {_redact_url(url)}","messagePattern":"deadline reached before (.+?) (.+?)","errorType":"exception","errorClass":"TimeoutError","httpStatus":null,"severity":"error","filePath":"scripts/virustotal_scan.py","lineNumber":396,"sourceCode":"        budget is spent so the caller can degrade to a warning row.\n\n        `max_attempts = 1` disables retrying, which is mandatory for a POST to a\n        single-use signed upload URL: replaying it can only ever be rejected.\n\n        `deadline` is checked BEFORE each attempt. One attempt can block for the\n        full socket timeout, so a loop that only checks afterwards can overrun the\n        caller's budget by minutes and get the whole step killed before it writes\n        a summary.\n        \"\"\"\n        headers = {\"x-apikey\": self._api_key, \"accept\": \"application/json\"}\n        if extra_headers:\n            headers.update(extra_headers)\n\n        backoff = self._request_interval if self._request_interval > 0 else 1.0\n        last_error = \"\"\n        for attempt in range(1, max_attempts + 1):\n            if deadline is not None and self._clock() >= deadline:\n                raise TimeoutError(f\"deadline reached before {method} {_redact_url(url)}\")\n            self._throttle(deadline)\n            # Re-check: pacing sleeps between the check above and the call below, so\n            # without this a request could start after the deadline and then block\n            # for the full socket timeout, overrunning the step's own budget.\n            if deadline is not None and self._clock() >= deadline:\n                raise TimeoutError(\n                    f\"deadline reached while pacing before {method} {_redact_url(url)}\"\n                )\n            try:\n                # Clamp the socket budget to what is left. Without this a call that\n                # starts just before the deadline can still block for the full\n                # socket timeout and consume the whole cushion the step relies on\n                # to write its summary.\n                socket_timeout = _SOCKET_TIMEOUT\n                if deadline is not None:\n                    socket_timeout = max(1.0, min(socket_timeout, deadline - self._clock()))\n                status, payload = self._transport(method, url, headers, body, socket_timeout)\n            except Exception as error:  # network layer, DNS, TLS, truncated read","sourceCodeStart":378,"sourceCodeEnd":414,"githubUrl":"https://github.com/unslothai/unsloth/blob/203007d19051dcd2ae33876786d117c99f6b0368/scripts/virustotal_scan.py#L378-L414","documentation":"Thrown in PlanReview.start (research-activity-panel.tsx:542-565) when a research run sits in awaiting_approval but latest.planHash is falsy after optionally saving plan edits via updateResearchPlan. The approval call (approveResearchRun) must send an opaque hash the backend computed over the plan; without it approval cannot prove the user reviewed that exact plan, so the flow aborts before the API call. Caught locally and shown as 'Could not start research'.","triggerScenarios":"Run status awaiting_approval whose run.planHash is null/undefined — either the snapshot fetched from the backend never carried plan_hash, or updateResearchPlan returned a run object lacking the field (older backend version, or a run created before the hash feature). Editing the draft makes no difference; the check applies to `latest` in both branches.","commonSituations":"Frontend newer than the backend (backend predates the approval-hash field); stale run snapshot in useResearchRunStore that predates a backend restart/re-creation of the run; backend bug omitting plan_hash on the awaiting_approval transition.","solutions":["Refresh the research run (refetch the session snapshot) so the store re-ingests a run with planHash populated.","Update the studio backend to a version that emits plan_hash on awaiting_approval runs.","If it persists, inspect the run object in the Network tab (GET /api/chat/research-runs/{id}) to confirm the backend omits plan_hash, and report it as a backend bug.","Cancel and re-create the research run so a fresh plan (with hash) is generated."],"exampleFix":null,"handlingStrategy":"type-guard","validationCode":"function canApprovePlan(run: ResearchRun): boolean {\n  return run.status === 'awaiting_approval' && typeof run.planHash === 'string' && run.planHash.length > 0;\n}","typeGuard":"function hasPlanHash(run: ResearchRun | undefined): run is ResearchRun & { planHash: string } {\n  return typeof run?.planHash === 'string' && run.planHash.length > 0;\n}","tryCatchPattern":"try {\n  await startResearch();\n} catch (error) {\n  if (error instanceof Error && error.message === 'The research plan is missing its approval hash.') {\n    await refetchResearchRun(runId); // re-ingest a fresh snapshot, then retry\n  } else throw error;\n}","preventionTips":["Disable the Start button until hasPlanHash(run) is true.","Keep frontend and backend versions in lockstep so plan_hash is always emitted.","Re-fetch the run snapshot when resuming research sessions restored from storage."],"tags":["research","approval","state","frontend","version-mismatch"],"backgroundTag":null,"analyzedSha":"203007d19051dcd2ae33876786d117c99f6b0368","analyzedAt":"2026-08-15T02:48:39.846Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}