Diagnostic reports

When an evaluation misbehaves, most of what explains it is in tables the UI never shows: the dispatch-gate flags on derivation_build, the InputsUnavailable attempt history behind a self-heal loop, worker_connection disconnect reasons, upstream probe metrics, and the resolved server settings that decide how any of it should be read.

A diagnostic report packages all of that into one SQLite file you can attach to a bug report, so a maintainer can answer the question without access to your instance.

Generating one

On a task page, open the three-dot menu on the evaluation panel and choose Diagnostic report. Four independent options control what goes in; every box ticked is the fullest report you may generate. The download starts when you press Generate, and building one takes a few minutes on a large evaluation.

The same thing over the API. The endpoint asks what to anonymise, which is the inverse of what the dialog asks:

curl -sO -J -H "Authorization: Bearer $TOKEN" \
  "https://gradient.example/api/v1/evals/$EVAL_ID/report?anonymize_packages=true"

What the options do

Option Default API parameter Effect
Include identities off anonymize_identities=true Off by default: repository URLs, project and task names, user emails, worker names and ids become per-report tokens (repo-a1b2, worker-7f3c).
Include package names on anonymize_packages=false On by default, because knowing which package broke is usually what makes a report useful. Unticked, the name half of store paths and flake attribute paths becomes a token.
Include build logs off include_logs=false Off by default: the full log of every failed or aborted attempt, carrying whatever the build printed. Successful builds are never included.
Include instance context on include_instance=true Worker fleet, upstream caches and the resolved server config. Requires the ManageWorkers permission.

The API keeps its own defaults for callers that omit a parameter (anonymize_identities=true, anonymize_packages=false, include_logs=true, include_instance=true); the dialog always sends all four explicitly.

Anonymisation is stable pseudonymisation, not deletion. The same input always maps to the same token within one report, so dependency and closure reasoning still works; a fresh salt per report means two reports of the same instance cannot be correlated. The salt is never written to the file.

Free text - build logs and commit messages - is rewritten against every pseudonym the report has minted, in one pass over the text. Both the pass and the pseudonym set it is compiled from are shared across every log, so the cost of anonymising a report grows with its size rather than with its size times its package count.

Nix store hashes are always preserved, even with everything anonymised. They are one-way, and they are what lets a maintainer check whether a path is available on a public cache, which is exactly the class of bug that motivates most reports.

What is never in the file

API keys, sessions, device-authorization records, worker registration token hashes, upstream cache API keys, user password hashes and forge app credentials are absent, not redacted. Every exported column is named explicitly in the extractor, so a table that later gains a secret column cannot start exporting it.

The report_manifest table records, for every table included, how many rows it holds against how many existed, what its scope selected and which filter was applied. A report generated without logs says so; it never looks like an evaluation that had none.

Read the scope column before drawing conclusions from a count. Only the evaluation's own tables are the evaluation's alone: builds hang off derivation_build anchors that are shared with every other evaluation that built the same derivation, so build_attempt, phase_event and the derivation* tables carry rows made for other evaluations, and the file will show attempts older than the evaluation itself. dispatched_job_phase is not one of those: it hangs off this evaluation's own dispatched jobs. worker_registration and upstream_metric describe the whole instance; worker_connection and worker_sample cover the workers that ran this evaluation, for as long as it ran.

Reading one

The file is an ordinary SQLite database, so any client opens it:

sqlite3 gradient-report-01a05a38-2026-09-01.db \
  "SELECT name, status FROM derivation_build JOIN derivation ON derivation.id = derivation_build.derivation"

dispatched_job_phase holds the worker's phase timeline, one row per span, nested through parent_seq. phase is the numeric discriminant; the names are listed in the job board page. To see where a job's time actually went:

sqlite3 gradient-report-01a05a38-2026-09-01.db \
  'SELECT j.worker_id, p.phase, p.end_ms - p.start_ms AS ms
     FROM dispatched_job_phase p
     JOIN dispatched_job j ON j.id = p.dispatched_job
    ORDER BY ms DESC LIMIT 20'

dispatched_job.outcome says how each job ended (0 completed, 1 failed); it is null for a job still running when the report was taken, and for one whose worker disconnected without reporting.

commit is a reserved word in SQLite, so the revision table needs quoting:

sqlite3 gradient-report-01a05a38-2026-09-01.db \
  'SELECT hash, author_name, message FROM "commit"'

The inspector adds curated views over the same file. It is called gradient-report, is on PATH inside nix develop, and runs standalone as nix run .#gradient-report:

gradient-report REPORT summary      status, timings, build and failure counts
gradient-report REPORT timeline     phase events, dispatches and attempts in order
gradient-report REPORT why-stuck    which gate each waiting anchor is held by
gradient-report REPORT failed       failed attempts; --log ATTEMPT dumps one
gradient-report REPORT workers      registration and connection history
gradient-report REPORT manifest     what the report contains and what it left out
gradient-report REPORT sql "QUERY"  raw access

why-stuck is the one to reach for first on a hung evaluation: for every anchor that never reached a terminal state it names which of edges_complete, closure_complete and drv_closure_cached is false, and lists the dependencies still unfinished underneath it.

The inspector refuses a report whose schema version it does not recognise rather than answering from whichever columns still happen to line up. If that happens, the report came from a newer Gradient than the tool.