99falls through to a normal parse, then rewrites the cache, so a stale rollup is never
1010shown; mtime is nanosecond-grained, so an in-place edit reliably invalidates.
1111
12- Only workflows() and model_breakdown() are intercepted -- they feed the first frame.
13- Everything else (workflow_nodes, tool_breakdown, message_timeline, supports_*, summary,
14- records_cost, demo, demo_scale, ...) delegates straight to the wrapped store, which
15- parses lazily the first time you actually drill into a session. So a warm start paints
16- instantly and only pays the parse if and when you open a session's detail.
12+ Only workflows(), model_breakdown() and records_cost are intercepted -- they feed the
13+ first frame (records_cost because some backends can only answer it by reading their
14+ whole corpus). Everything else (workflow_nodes, tool_breakdown, message_timeline,
15+ supports_*, summary, demo, demo_scale, ...) delegates straight to the wrapped store,
16+ which parses lazily the first time you actually drill into a session. So a warm start
17+ paints instantly and only pays the parse if and when you open a session's detail.
1718
1819The cache is disabled under --demo (demo never persists, and its per-process scale must
1920not be baked in) and --no-cache; sources.make_store applies the wrapper.
2829
2930from opentab .models import Workflow
3031
31- CACHE_VERSION = 2 # bump when the cached payload shape changes (invalidates old files)
32+ CACHE_VERSION = 3 # bump when the cached payload shape changes (invalidates old files)
3233
3334
3435def cache_dir () -> str :
@@ -49,10 +50,21 @@ def __init__(self, store, cache_id: str, args: argparse.Namespace):
4950 self .served_from_cache : bool | None = None # set by workflows(); read by --timings
5051
5152 # Anything not intercepted below is the wrapped store's -- workflow_nodes, the Turns/
52- # Tools extras, supports_*, records_cost, demo, source_name, summary, and so on.
53+ # Tools extras, supports_*, demo, source_name, summary, and so on.
5354 def __getattr__ (self , name ):
5455 return getattr (self ._store , name )
5556
57+ @property
58+ def records_cost (self ) -> bool :
59+ # Served from the cache on a fingerprint hit: some backends (pi/OpenClaw/CSV/
60+ # JSONL) can only answer this by reading their whole corpus, which would defeat
61+ # the warm start. A miss (or a pre-v3 cache) delegates like __getattr__ does.
62+ if self ._disk is not None and "records_cost" in self ._disk :
63+ fp = self ._live_fp if self ._live_fp is not None else self ._fingerprint ()
64+ if self ._disk .get ("fingerprint" ) == fp :
65+ return bool (self ._disk ["records_cost" ])
66+ return getattr (self ._store , "records_cost" , True )
67+
5668 # --- fingerprint / cache file -------------------------------------------------
5769 def _fingerprint (self ) -> list :
5870 # Sorted [path, size, mtime_ns] over the backend's inputs. Lists (not tuples) so
@@ -89,6 +101,9 @@ def _write(self, fingerprint: list, workflows: list, model_breakdown: list) -> N
89101 "version" : CACHE_VERSION ,
90102 "source" : self ._source ,
91103 "fingerprint" : fingerprint ,
104+ # Cheap here: the backend just parsed, so a lazy records_cost derives from
105+ # that parse instead of running its full-corpus probe.
106+ "records_cost" : bool (getattr (self ._store , "records_cost" , True )),
92107 "workflows" : workflows ,
93108 "model_breakdown" : model_breakdown ,
94109 }
@@ -108,9 +123,14 @@ def workflows(self) -> list:
108123 # fingerprint (the common warm start / no-op reload) serves the cache untouched.
109124 self ._live_fp = self ._fingerprint ()
110125 if self ._disk is not None and self ._disk .get ("fingerprint" ) == self ._live_fp :
111- self ._fresh_wf = None # a hit: nothing new to write
112- self .served_from_cache = True
113- return [Workflow (** row ) for row in self ._disk ["workflows" ]]
126+ try :
127+ rows = [Workflow (** row ) for row in self ._disk ["workflows" ]]
128+ except TypeError :
129+ self ._disk = None # cached fields drifted from the dataclass: reparse
130+ else :
131+ self ._fresh_wf = None # a hit: nothing new to write
132+ self .served_from_cache = True
133+ return rows
114134 workflows = self ._store .workflows () # miss: real parse
115135 self ._fresh_wf = [asdict (w ) for w in workflows ]
116136 self .served_from_cache = False
0 commit comments