perf(webui/session-load-latency) tier3d: Greptile P1 followups — inconclusive scene check + full-content config verification

Two Greptile P1 comments from review of b5c0447a (tier3c):

1. Inconclusive Prefix Stays Fresh (discussion_r3548477859):
   When _persisted_session_meta_prefix() returns None and cached has scene
   records, the fast-path skipped scene comparison entirely and returned
   False (cache fresh) for inactive equal-count sessions. Added
   _scene_check_inconclusive flag that forces fall-through to the full
   Session.load_metadata_only() comparison when the cheap prefix read
   fails, preventing stale anchor_activity_scenes.

2. Prefix Check Misses Edits (discussion_r3548477915):
   The 128-char content-prefix backstop in _read_profile_config_cached
   only detected edits within the first 128 bytes of config.yaml. Replaced
   with a full-content comparison that reads the entire file (~1-10KB)
   and compares it to a copy stored in the cache entry. This detects
   edits anywhere in the file at a fraction of the cost of yaml.safe_load().
   Updated cache type hint from tuple[float, str|None, dict] to
   tuple[float, str, dict].

Tests: 108 passed (41 reconciliation + 39 anchor-scene + 28 profile-config)
This commit is contained in:
Konstantin M 2026-07-09 03:08:11 +00:00
parent b5c0447a4a
commit db99f72505
2 changed files with 46 additions and 26 deletions

View file

@ -3127,6 +3127,10 @@ def _cached_session_lags_disk(cached) -> bool:
cached_scenes = getattr(cached, 'anchor_activity_scenes', None) or {}
if not isinstance(cached_scenes, dict):
cached_scenes = {}
# Track whether the cheap scene check was inconclusive — when it is,
# we must fall through to the full metadata comparison instead of
# returning False for inactive sessions with matching counts.
_scene_check_inconclusive = False
if cached_scenes:
disk_meta_quick = _persisted_session_meta_prefix(sid)
if disk_meta_quick is not None:
@ -3160,6 +3164,13 @@ def _cached_session_lags_disk(cached) -> bool:
if _max_updated(disk_scenes) > _anchor_scene_records_updated_at(cached):
return True
# disk has no scenes, cache does -> cache is ahead; keep it.
else:
# Can't cheaply verify scene freshness from disk. The prefix
# reader may have failed while _persisted_message_count
# succeeded via index fallback. Fall through to the full
# metadata load so we don't serve stale scenes on the next
# equal-count inactive session path. Greptile P1.
_scene_check_inconclusive = True
else:
# Cached session has no scene records. Check if disk has gained
# the first scene record — without this the fast-path would miss
@ -3173,6 +3184,11 @@ def _cached_session_lags_disk(cached) -> bool:
# Active session: messages may be in flight; fall through to the
# full check to be safe.
pass
elif _scene_check_inconclusive:
# Could not cheaply verify scene freshness from disk; fall through
# to the full metadata comparison rather than returning False
# (which would serve a potentially stale cache). Greptile P1.
pass
else:
# Inactive session, count matches, scene records match — cache is
# at parity with disk.

View file

@ -6314,32 +6314,34 @@ def _read_profile_model_config(
# new inode on Linux). mtime+size auto-invalidates on in-place edits; a 60s TTL
# is the backstop in case of coarse mtime resolution (some network filesystems
# round mtime to whole seconds — the size guard catches a write of equal-length
# bytes within the same second). On cache hit, a quick content-prefix comparison
# (first 128 chars) catches in-place rewrites that inode+mtime+size miss on
# coarse filesystems (Greptile P1, PR#5803 discussion_r3548069113).
# Reads are guarded by a single Lock to keep the hot path simple;
# the underlying yaml.safe_load is the slow step, not the lock, so contention
# is bounded.
_PROFILE_CONFIG_CACHE: "dict[tuple, tuple[float, str | None, dict]]" = {}
# bytes within the same second). On cache hit, a full-content comparison catches
# any in-place rewrite that inode+mtime+size missed (Greptile P1, PR#5803
# discussion_r3548477915). Reading and comparing the full file content (~1-10KB)
# is much cheaper than yaml.safe_load(). Reads are guarded by a single Lock to
# keep the hot path simple; the underlying yaml.safe_load is the slow step, not
# the lock, so contention is bounded.
_PROFILE_CONFIG_CACHE: "dict[tuple, tuple[float, str, dict]]" = {}
_PROFILE_CONFIG_CACHE_TTL_SECONDS = 60.0
_PROFILE_CONFIG_CACHE_LOCK = threading.Lock()
def _read_profile_config_cached(profile_name: str, cfg_path: str) -> dict | None:
"""Return parsed profile config, caching by (inode, mtime, size) with
TTL backstop and content-prefix verification.
TTL backstop and full-content verification.
The content-prefix comparison (first 128 chars of the file) catches
in-place rewrites where inode+mtime+size are identical on coarse
filesystems — most editors use atomic-rename (new inode), but tools
like sed -i or file.write() in-place can keep the same inode.
The full-content comparison reads the current file and compares it to a
copy stored in the cache entry. This catches any in-place rewrite where
inode+mtime+size are identical, regardless of where in the file the change
occurs — unlike a fixed-length prefix comparison, edits to fields after the
first N characters are always detected. Reading and comparing the full file
content (~1-10KB for a typical config.yaml) is much cheaper than
yaml.safe_load().
NOTE: This is a best-effort backstop, not a full integrity check.
Edits that change bytes only after the first 128 characters while
keeping inode+mtime+size identical are not detected until the TTL
expires. This trade-off is acceptable because the TTL is only 60s
and the common case (atomic-rename editors) is fully handled by
the inode key component.
NOTE: The cache key uses inode+mtime+size to handle the common cases
(atomic-rename editors -> new inode; in-place editors -> mtime/size
change). The full-content comparison is a backstop for the rare case where
all three collide (e.g., sed -i on a filesystem with coarse mtime
resolution, writing the same byte count).
"""
try:
st = os.stat(cfg_path)
@ -6353,17 +6355,20 @@ def _read_profile_config_cached(profile_name: str, cfg_path: str) -> dict | None
with _PROFILE_CONFIG_CACHE_LOCK:
cached = _PROFILE_CONFIG_CACHE.get(key)
if cached is not None:
cached_at, cached_prefix, cached_dict = cached
cached_at, cached_content, cached_dict = cached
if (now - cached_at) <= _PROFILE_CONFIG_CACHE_TTL_SECONDS:
# Cheap content verification: read first 128 chars to catch
# in-place rewrites that inode+mtime+size don't detect.
_current_prefix = None
# Full content comparison catches any in-place rewrite where
# inode+mtime+size are identical but the file content changed.
# Reading and comparing the full file (~1-10KB) is cheaper than
# yaml.safe_load(). Unlike a fixed-length prefix, this detects
# edits anywhere in the file. Greptile P1 (PR#5803).
_current_content = None
try:
with open(cfg_path, "r", encoding="utf-8") as _f:
_current_prefix = _f.read(128)
_current_content = _f.read()
except Exception:
pass
if _current_prefix == cached_prefix:
if _current_content == cached_content:
return cached_dict
# Content changed while key collided — fall through to re-parse
import yaml
@ -6375,9 +6380,8 @@ def _read_profile_config_cached(profile_name: str, cfg_path: str) -> dict | None
return None
if not isinstance(parsed, dict):
return None
content_prefix = content[:128] # first 128 chars for change detection
with _PROFILE_CONFIG_CACHE_LOCK:
_PROFILE_CONFIG_CACHE[key] = (now, content_prefix, parsed)
_PROFILE_CONFIG_CACHE[key] = (now, content, parsed)
# Cap the cache at 32 entries; profiles are bounded in practice
# and unbounded growth would be a leak.
if len(_PROFILE_CONFIG_CACHE) > 32: