Record the inputs a run actually starts from, not only the ones passed

A run submitted without explicit inputs recorded `params = {}`: Port
initials filled the values at node level and were never written back, so an
exported row had a blank `param.*` cell and the runs listing could not tell
a run that took every default from one submitted with those same numbers.
Declared initials are now folded in at submit, explicit values winning, and
the run-level seed still wins over a declared one.

`params_digest` is computed over the resolved values, so it changes shape
once: a run recorded before this does not dedupe against a newer identical
submit, and its stage-cache entries miss once.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Hra4ndWMCLU5F3KjUuVAc
This commit is contained in:
2026-08-29 13:45:49 +02:00
co-authored by Claude Opus 5
parent 2d654fd943
commit c050a7a52c
2 changed files with 71 additions and 0 deletions
+14
View File
@@ -846,6 +846,20 @@ class RunService:
# reference a python caller would have passed and every later reader —
# the digest, the cache, the run detail — sees one spelling.
params = resolve_references(flow, params, self._artifacts)
# What the run actually starts from, not only what was passed: an input
# left out takes its declared value, and a row that records `{}` cannot
# say which. Folded literally — an initial is a value from the
# definition, never a reference to resolve. The run-level seed still
# wins over a declared one, the way `seed_values` has it.
params = {
**{
declared.spec.name: declared.initial
for declared in flow.inputs
if declared.initial is not None
and not (declared.spec.name == "seed" and seed is not None)
},
**params,
}
# Checked here rather than in the driver: a caller who mistyped a
# parameter should be told now, not by a run that fails in a minute.
seed_values(flow, params, seed)
+57
View File
@@ -400,6 +400,63 @@ def test_a_repeated_submit_returns_the_run_it_already_made():
assert again.id == "dedup-1"
class _OneFlow:
"""A controller that has exactly one flow and no engine behind it."""
def __init__(self, flow):
self.store = self
self._flow = flow
def read_flow(self, name, draft=False):
return self._flow
def head(self):
return ""
class _Collect:
def __init__(self):
self.items = []
def add(self, item):
self.items.append(item)
def test_a_run_records_the_inputs_it_actually_starts_from():
"""An input left out takes its declared value, and the row says so.
`params = {}` could not tell a run that took every default from one
submitted with those same numbers spelled out — and an export of the
first had a blank cell where its `lr` should be.
"""
flow = FlowDef(
name="study",
mode="batch",
inputs=[
FlowInput(
spec=MessageSpec(name="lr", dtype=DType.FLOAT), initial=0.01
),
FlowInput(spec=MessageSpec(name="epochs", dtype=DType.INT)),
],
)
service = RunService(controller=_OneFlow(flow), queue=_Collect())
made = []
try:
defaulted = service.submit("study", {"epochs": 5})
made.append(defaulted.id)
assert defaulted.params == {"lr": 0.01, "epochs": 5}
# Spelling out the declared value is the same run, and now reads as it.
spelled = service.submit("study", {"lr": 0.01, "epochs": 5})
made.append(spelled.id)
assert spelled.params_digest == defaulted.params_digest
finally:
with Session(db_engine) as session:
for run in session.exec(select(Run).where(col(Run.id).in_(made))).all():
session.delete(run)
session.commit()
def test_a_key_nobody_used_submits_normally(
client, superuser_token_headers, monkeypatch
):