A restored node whose source run is gone says its curve was lost

The run detail works out curve_lost per node when read, from whether its
cached_from run still exists. The run page names the deleted run in place
of the generic NO_CURVE note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WgBfkm4rCmtd1T4vHWjEPx
This commit is contained in:
2026-10-15 10:22:09 +02:00
co-authored by Claude Opus 5.5
parent df2620cede
commit f0bddc56a4
7 changed files with 50 additions and 10 deletions
+15 -2
View File
@@ -146,6 +146,9 @@ class RunNodeRow(BaseModel):
#: Which run it was restored from, when it was. That run is also where this
#: node's series was recorded.
cached_from: str = ""
#: That run has since been deleted, and this node's curve went with it. Its
#: outputs are still on this run's result.
curve_lost: bool = False
class ArtifactRow(BaseModel):
@@ -781,6 +784,16 @@ def read_run(run_id: str, session: SessionDep) -> Any:
).all()
detail = RunDetail.model_validate(run, from_attributes=True)
detail.nodes = [RunNodeRow.model_validate(n, from_attributes=True) for n in nodes]
# Worked out here, not stored: a source run is deleted long after the
# nodes restored from it were written.
sources = {n.cached_from for n in nodes if n.cached_from}
kept = (
set(session.exec(select(col(Run.id)).where(col(Run.id).in_(sources))))
if sources
else set()
)
for row in detail.nodes:
row.curve_lost = bool(row.cached_from) and row.cached_from not in kept
detail.artifacts = [
ArtifactRow.model_validate(a, from_attributes=True) for a in artifacts
]
@@ -824,8 +837,8 @@ def _drop_runs(session: Session, chosen: Any) -> None:
Two things this costs, both deliberate. ``RunNode.outputs`` *is* the stage
cache, so a later run loses hits these would have served. And a node
restored from one of them points here through ``cached_from`` — ``_series``
already reads a missing source as an empty curve, which is what ``NO_CURVE``
explains on the screen. The artifact bytes need no help: ``sweep_artifacts``
already reads a missing source as an empty curve, and ``read_run`` marks
such a node ``curve_lost``. The artifact bytes need no help: ``sweep_artifacts``
keeps whatever a ``run_artifact`` row or a live message still names, so
dropping the rows is enough and the hourly sweep reclaims the blobs.
"""
+5
View File
@@ -1108,6 +1108,11 @@ def test_a_curve_whose_run_is_gone_is_empty_rather_than_an_error(
)
assert answer.status_code == 200
assert answer.json() == []
# And the run says why, rather than leaving an empty chart to explain itself.
(node,) = client.get(
f"{settings.API_V1_STR}/runs/reuse-1", headers=superuser_token_headers
).json()["nodes"]
assert node["curve_lost"] is True
def _metric(run_id: str, name: str, step: int, value: float, ts: float) -> RunMetric: