Key the stage cache by the node's input names, not the flow's

A cache key held qualified input names, so the same node reading the same
values through two flows keyed differently and only a node with no inputs
could ever hit across one. The fingerprint beside the key already says what
the node is, and it has been flow-agnostic since it moved ahead of
assign_flow — the names were the last thing tying an entry to one flow.

Inputs now reduce by the node's own name for them; a name belonging to
another flow keeps its prefix, since reading it is part of what the
execution is. Every stored entry misses once and is re-run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-26 22:41:35 +02:00
co-authored by Claude Opus 5
parent 647644ebbd
commit 001ec7b282
2 changed files with 27 additions and 6 deletions
+11 -3
View File
@@ -151,16 +151,24 @@ class RunCacheLookup(Protocol):
"""What an equal execution produced, or None when there is no entry."""
def run_cache_key(fingerprint: str, inputs: dict[str, Any]) -> str:
def run_cache_key(fingerprint: str, inputs: dict[str, Any], flow: str = "") -> str:
"""What this node, with these inputs, is known by.
An artifact input counts as its digest: the reference carries a name and a
size beside it, and the same bytes under another name are the same input.
A value JSON cannot carry cannot be part of a key, and a node reading one
is simply not cacheable.
Inputs are keyed by the *node's* name for them, not the flow's: the same
node reading the same values through ``study`` and through ``quick`` did
the same work, and the fingerprint beside it already says what the node is.
A name belonging to some other flow keeps its prefix — reading
``other.metric`` is part of what makes this execution what it is.
"""
reduced = {
name: value["digest"] if is_reference(value) else value
(name.removeprefix(f"{flow}.") if flow else name): (
value["digest"] if is_reference(value) else value
)
for name, value in inputs.items()
}
try:
@@ -932,7 +940,7 @@ class Pipeline:
key = ""
if self.run_cache is not None and node.fingerprint:
key = run_cache_key(node.fingerprint, inputs)
key = run_cache_key(node.fingerprint, inputs, node.flow)
if key:
hit, restored = self._from_cache(node, key, state, entry_id)
if hit: