unsloth/studio/backend/auth
Maheswar Kumar d9e033a9b5
studio: fix deep research progress reporting, loopback auth and model retries (#8129)
* studio: fix deep research progress reporting, loopback auth and model retries

Deep Research looked broken from the frontend even when the backend was working.
The plan was ready long before the panel showed it, the header sat on
"0 sources · 0 actions", and every model call opened the API monitor.

Stall: routes/research_runs.research_events waited on db.wait_for_events through
the default executor, so open followers starved the supervisor's own to_thread
writes. The wait now runs on a dedicated _EVENT_WAIT_EXECUTOR, with db.get_run
left on the default executor so a short read never queues behind parked waits.
chat-adapter.ts also owned the stream, so a slow reader froze the card;
research-run-store.ts owns it now and the adapter reads through watchResearchRun.

Progress: phase.started/progress/ended bracket planning, each decision, the audit
and synthesis. Plan step titles stream out of the partial JSON. The header shows
the running step without an invented denominator, and a completed fetch step no
longer claims it found nothing.

Loopback auth: auth.storage.is_internal_api_key lets _request_used_api_key
exclude Studio's own workflow keys, so research steps stop opening the API
monitor. The answer is memoized, since it runs on the event loop per request.

Model refusals: each wait takes modelTimeoutSeconds / (_MAX_MODEL_WAITS + 1) so
the wall clock no longer buries the real 400; a 404 that names the model is
capped at _NAMED_MODEL_WAIT_SECONDS; and 503 model_switch_failed honours
Retry-After instead of giving up in three seconds.

Search: _web_search separates a rate limit, a timeout and an empty sweep instead
of collapsing all three into "Search failed". _research_step_failed treats an
empty result with no knowledge-base evidence as a failed step.

Autosave: syncExportedRepositoryToBackend echoes the stored copy of a research
report and its prompt, so the 409 no longer discards the whole thread payload.
parentId stays the client's, or a delete would persist a link to a pruned row.

Cleanup: prompts, redaction, citations and parsing move to core/research/, and
_completion, research_runs_db.update_step and stopResearchRunFollower are gone.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* studio: let a pruned parent relink a research prompt instead of 409ing

Deleting any message above a research turn still dropped the whole autosave. The client
relinks the protected prompt to the deleted node's parent, and the guard compared that
parentId against the stored one, so sync_chat_messages raised and the route returned 409.

The prune set is now computed before the guard runs, and a parentId change on a protected
message is accepted only when its stored parent is being deleted by the same request and the
new parent is exactly the surviving ancestor the server itself walks to. Content, metadata,
attachments, role and createdAt are compared as before, and a relink to any other target is
still refused.

* studio: keep the new research tests on the declared Python floor

Both files annotate with PEP 604 unions in signatures, which Python evaluates at import, so
they pushed tests/test_python39_compatibility.py past its ratchet and would TypeError on the
3.9 floor declared in pyproject.toml.

* studio: split the agent-action assertions into their own test

They were the only reason the surviving scope had to re-point its imports at the extracted
modules, which the import-hoist verifier reads as a rename.

* studio: clear the api key origin cache alongside the hash cache

is_internal_api_key sizes itself against the hash cache, so clearing one without the other
lets the origin cache grow past its bound. Deep Research mints a fresh internal key per model
call, so it is the workload that reaches it.

* studio: assert search failures against the real ddgs exception classes

The tests defined their own DDGSException, RatelimitException and TimeoutException, so they
passed by construction. ddgs is unpinned and has renamed these before, and the classifier
matches on the class name.

* studio: surface a failed stored-message read instead of syncing without it

Swallowing it sent the unreconciled payload, which the backend rejects wholesale, so a
transient read failure came back as a confusing 409 about server-managed messages.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* studio: normalize the surviving-parent walk the way its caller reads parentId

The helper returned the stored parent_id verbatim while the guard compares against
`message.get("parentId") or None`, so an empty stored parent_id made the two disagree and
refused a legitimate relink. A self-link left behind by a corrupt chain now resolves to the
root instead of handing back the message's own id.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: danielhanchen <danielhanchen@gmail.com>
2026-08-07 23:34:38 -07:00
..
.gitkeep root studio folder 2026-02-02 09:13:49 +00:00
__init__.py Studio: make code comments and docstrings more succinct (#6029) 2026-06-08 23:07:28 -07:00
authentication.py feat(studio): add settings-managed remote access (#7875) 2026-08-06 07:53:44 -07:00
bootstrap_timeout.py Replace standalone Studio wording with Unsloth (#7221) 2026-07-19 00:47:04 -07:00
hashing.py Reduce and tighten code comments and docstrings repo-wide (#6095) 2026-06-08 23:09:51 -07:00
storage.py studio: fix deep research progress reporting, loopback auth and model retries (#8129) 2026-08-07 23:34:38 -07:00
terminal_prompt.py Studio: reject whitespace-only passwords (#7341) 2026-07-23 00:44:37 -07:00