1616SCENARIO_TYPES = ("fixed-seq-len" , "agentic-coding" )
1717
1818
19+ def _freeze_config_value (value ):
20+ """Convert JSON-shaped config values into deterministic hashable values."""
21+ if isinstance (value , dict ):
22+ return tuple (
23+ sorted ((key , _freeze_config_value (item )) for key , item in value .items ())
24+ )
25+ if isinstance (value , list ):
26+ return tuple (_freeze_config_value (item ) for item in value )
27+ return value
28+
29+
1930def get_added_lines (base_ref : str , head_ref : str , filepath : str ) -> str :
2031 result = subprocess .run (
2132 ["git" , "diff" , base_ref , head_ref , "--" , filepath ],
@@ -54,8 +65,8 @@ def trim_conc(entries: list[dict]) -> list[dict]:
5465 the source ordering of ``conc-list`` / ``conc-start``.
5566
5667 Input comes from ``json.loads(subprocess.stdout)`` so ``conc`` is always
57- ``int`` (single-node) or ``list`` (multi-node); other single-node fields
58- are hashable scalars .
68+ ``int`` (single-node) or ``list`` (multi-node). Other fields may contain
69+ nested dictionaries or lists, such as KV-offload backend metadata .
5970
6071 - Single-node entries: group by every other field and keep only the entry
6172 with the lowest ``conc`` per group.
@@ -72,7 +83,13 @@ def trim_conc(entries: list[dict]) -> list[dict]:
7283 out .append (entry )
7384 continue
7485
75- key = tuple (sorted ((k , v ) for k , v in entry .items () if k != "conc" ))
86+ key = tuple (
87+ sorted (
88+ (k , _freeze_config_value (v ))
89+ for k , v in entry .items ()
90+ if k != "conc"
91+ )
92+ )
7693 groups .setdefault (key , []).append (len (out ))
7794 out .append (entry )
7895
0 commit comments