[object Object]

← back to Terminal Status

iterm_sessions: cross-process cache collapses the enum herd + blind falls back to cache, not green (TK-11879)

2a0da771bdbf70d088703bfe1701f13e25823fd8 · 2026-09-17 07:30:06 -0700 · Steve Abrams

~49 sessions each shelled an iTerm2 window/tab/session osascript at start;
iTerm's single AppleEvent handler serialized them under load and later ones
returned rc 1 (628 'osascript returncode 1' blinds/7d). A blind returned {}
which made the start path paint a confidently-wrong GREEN instead of restoring
a semantic dot.

Mirror the proven ps disk-cache (TK-11398/11831): a short-TTL (30s) cross-process
JSON cache of the {tty:title} map so only the first sibling enumerates and the
rest read the file. On a blind, fall back to a recently-cached map (<=300s) as
'stale' instead of {}; only when NO cache exists record enum_blind + return
unavailable (honest NOT-MEASURED). start restore gate accepts a fresh 'cached'
map. Never cache an empty rc-0 enumeration (would poison siblings silently).

+7 tests incl. the negative fault-injection (blind with no cache still records
enum_blind). All 56 pass. Live-verified: 1 enumerate then cache HIT.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jwp6QVPb7cuafHPP7uLyi8

Files touched

Diff

commit 2a0da771bdbf70d088703bfe1701f13e25823fd8
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Sep 17 07:30:06 2026 -0700

    iterm_sessions: cross-process cache collapses the enum herd + blind falls back to cache, not green (TK-11879)
    
    ~49 sessions each shelled an iTerm2 window/tab/session osascript at start;
    iTerm's single AppleEvent handler serialized them under load and later ones
    returned rc 1 (628 'osascript returncode 1' blinds/7d). A blind returned {}
    which made the start path paint a confidently-wrong GREEN instead of restoring
    a semantic dot.
    
    Mirror the proven ps disk-cache (TK-11398/11831): a short-TTL (30s) cross-process
    JSON cache of the {tty:title} map so only the first sibling enumerates and the
    rest read the file. On a blind, fall back to a recently-cached map (<=300s) as
    'stale' instead of {}; only when NO cache exists record enum_blind + return
    unavailable (honest NOT-MEASURED). start restore gate accepts a fresh 'cached'
    map. Never cache an empty rc-0 enumeration (would poison siblings silently).
    
    +7 tests incl. the negative fault-injection (blind with no cache still records
    enum_blind). All 56 pass. Live-verified: 1 enumerate then cache HIT.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01Jwp6QVPb7cuafHPP7uLyi8
---
 terminal_status.py      | 122 ++++++++++++++++++++++++++++++++++++++++++++----
 test_terminal_status.py | 104 +++++++++++++++++++++++++++++++++++++++++
 2 files changed, 216 insertions(+), 10 deletions(-)

diff --git a/terminal_status.py b/terminal_status.py
index e20d67e..ba9c530 100644
--- a/terminal_status.py
+++ b/terminal_status.py
@@ -1050,7 +1050,95 @@ class Store:
             return title
 
 
-def iterm_sessions():
+# TK-11879: the iTerm2 window/tab/session enumeration below is served by iTerm's
+# SINGLE AppleEvent handler, so when ~49 interactive sessions each shell it at start
+# under host load they serialize inside iTerm and the later ones time out or return
+# rc 1 (628 'osascript returncode 1' blinds in one 7d window). That is the identical
+# herd-contention class the ps process-table scan hit, and the fix is the same proven
+# one: a short-TTL cross-process disk cache (TK-11398/TK-11831). The FIRST session in
+# a window enumerates and publishes the {tty: title} map; every sibling that starts
+# within the TTL reads the file and never adds a second AppleEvent to the busy handler.
+# When a live enumeration DOES go blind, a recently cached map is a graceful fallback:
+# a real (if slightly stale) session map beats the {} that makes `start` fall through
+# to a confidently-wrong green. Only when NO usable cache exists is it a genuine blind.
+_ITERM_TTL = float(os.environ.get("TERMINAL_STATUS_ITERM_TTL", "30"))
+# How much older than the fresh TTL a cached map may be and still serve as the
+# blind-fallback. Bounded so a long-abandoned map is not trusted forever.
+_ITERM_STALE_MAX = float(os.environ.get("TERMINAL_STATUS_ITERM_STALE_MAX", "300"))
+
+
+def _iterm_cache_path():
+    return Path(tempfile.gettempdir()) / f"terminal-status-iterm-{os.getuid()}.json"
+
+
+def _iterm_cache_get(max_age):
+    """Most recent sibling enumeration within max_age seconds, or None. Never raises.
+    Returns (sessions_dict, age_seconds)."""
+    try:
+        path = _iterm_cache_path()
+        st = path.stat()
+        if st.st_uid != os.getuid():          # never trust another user's file
+            return None
+        age = time.time() - st.st_mtime
+        if age >= max_age:
+            return None
+        payload = json.loads(path.read_text())
+        sessions = payload.get("sessions")
+        if not isinstance(sessions, dict):
+            return None
+        clean = {k: v for k, v in sessions.items()
+                 if isinstance(k, str) and isinstance(v, str) and TTY.fullmatch(k)}
+        return clean, age
+    except (OSError, ValueError, TypeError, KeyError):
+        return None
+
+
+def _iterm_cache_put(sessions):
+    """Publish this enumeration for sibling invocations. Best effort, never raises.
+    Only SUCCESSFUL enumerations (rc 0) are ever cached -- a blind never writes here,
+    so the fallback map is always a map iTerm actually returned."""
+    try:
+        path = _iterm_cache_path()
+        fd, tmp = tempfile.mkstemp(dir=str(path.parent), prefix=".ts-iterm-")
+        try:
+            with os.fdopen(fd, "w") as handle:
+                json.dump({"sessions": sessions}, handle)
+            os.chmod(tmp, 0o600)
+            os.replace(tmp, path)             # atomic; readers see whole files
+        except BaseException:
+            os.unlink(tmp)
+            raise
+    except OSError:
+        pass
+
+
+def _iterm_blind(reason):
+    """A live enumeration failed. Prefer a recent cached map (graceful degrade to a
+    REAL session map, not the {} that makes `start` paint a confidently-wrong green,
+    TK-11879). Only when NO usable cache exists is this a genuine blind: record
+    enum_blind (an honest NOT-MEASURED signal -- the residual wrong-green risk) and
+    return unavailable. A recovered blind is deliberately NOT recorded, because with
+    a real fallback map there is no wrong paint to warn about."""
+    fallback = _iterm_cache_get(_ITERM_STALE_MAX)
+    if fallback is not None:
+        if os.environ.get("TERMINAL_STATUS_DEBUG"):
+            print("iterm-cache: BLIND (%s) -> stale fallback (%.1fs)"
+                  % (reason, fallback[1]), file=sys.stderr)
+        return fallback[0], "stale"
+    _record_enum_blind(reason)
+    return {}, "unavailable"
+
+
+def iterm_sessions(fresh=False):
+    # Serve a fresh sibling enumeration from the cross-process cache without touching
+    # iTerm at all (herd collapse). `fresh=True` forces a live enumeration (testability
+    # seam + a way to warm the cache deliberately); callers never set it.
+    if not fresh:
+        hit = _iterm_cache_get(_ITERM_TTL)
+        if hit is not None:
+            if os.environ.get("TERMINAL_STATUS_DEBUG"):
+                print("iterm-cache: HIT (%.1fs)" % hit[1], file=sys.stderr)
+            return hit[0], "cached"
     script = """
 set sep to ASCII character 9
 tell application "iTerm2"
@@ -1070,24 +1158,33 @@ end tell
         # enumeration cost scales with tab count (~49 here) and this box has hit
         # load 53.78, so an 8s ceiling was trippable. Raised to 30s -- NOT 60s
         # like the ps call, because this runs at session start and a longer hang
-        # would delay startup. A timeout here is a genuine blind spot, not a
-        # benign empty result: it makes `start` fall through and paint green
-        # instead of restoring a sticky semantic dot (a confidently-wrong paint),
-        # so it is now recorded rather than silently swallowed.
+        # would delay startup. A blind here is a genuine blind spot, not a benign
+        # empty result, so it degrades to a cached map (TK-11879) or, failing that,
+        # is recorded rather than silently swallowed.
         result = subprocess.run(["osascript", "-e", script], capture_output=True,
                                 text=True, timeout=30)
         if result.returncode:
-            _record_enum_blind("osascript returncode " + str(result.returncode))
-            return {}, "unavailable"
+            return _iterm_blind("osascript returncode " + str(result.returncode))
     except (OSError, subprocess.TimeoutExpired) as exc:
-        _record_enum_blind(type(exc).__name__ + ": " + str(exc)[:120])
-        return {}, "unavailable"
+        return _iterm_blind(type(exc).__name__ + ": " + str(exc)[:120])
     sessions = {}
     for line in result.stdout.splitlines():
         tty, sep, title = line.partition("\t")
         tty = tty.removeprefix("/dev/")
         if sep and TTY.fullmatch(tty):
             sessions[tty] = title
+    # Only cache a NON-EMPTY map. An rc-0 enumeration that returns zero sessions is
+    # never legitimate here -- the asking Claude session is itself a session, so an
+    # empty result is a partial/racey answer (iTerm mid-launch). Caching it would
+    # serve siblings a `cached {}` for a whole TTL -> the same green fall-through this
+    # fix exists to kill, and silently (no enum_blind). Leave the cache untouched so
+    # the next sibling re-enumerates; this call still returns its own honest result.
+    if sessions:
+        _iterm_cache_put(sessions)
+    if os.environ.get("TERMINAL_STATUS_DEBUG"):
+        print("iterm-cache: MISS -> enumerated %d sessions%s"
+              % (len(sessions), " (empty, not cached)" if not sessions else ""),
+              file=sys.stderr)
     return sessions, "available"
 
 
@@ -1418,7 +1515,12 @@ def main(argv=None):
             elif reason == "missing":
                 sessions, api = iterm_sessions()
                 legacy, legacy_reason = store.legacy_status(owner, sessions.get(owner.tty, ""))
-                if legacy is not None and api == "available":
+                # A cross-process cache HIT ("cached", <30s old) is as trustworthy as a
+                # live enumeration for the legacy-conflict check, so it restores a sticky
+                # semantic dot instead of falling through to green (TK-11879). "stale" and
+                # "unavailable" stay excluded — an older live title is not a safe basis for
+                # deciding a conflict, so those keep the existing floor/raise behaviour.
+                if legacy is not None and api in ("available", "cached"):
                     record = store.set(owner, legacy["state"], legacy["label"])
                 elif legacy_reason in ("missing", "legacy_stale", "legacy_empty"):
                     record = store.set(owner, "green")
diff --git a/test_terminal_status.py b/test_terminal_status.py
index 8e30485..14011f9 100644
--- a/test_terminal_status.py
+++ b/test_terminal_status.py
@@ -783,5 +783,109 @@ class EnrichmentCacheTests(unittest.TestCase):
             self.assertEqual(len(calls), 2, "an expired entry must be re-read")
 
 
+class ItermEnumCacheTests(unittest.TestCase):
+    """TK-11879: the iTerm session enumeration collapses the herd through a
+    cross-process cache and degrades to that cache when a live enumeration goes
+    blind, instead of the {} -> confidently-wrong-green fall-through."""
+
+    def setUp(self):
+        self.temp = tempfile.TemporaryDirectory()
+        self.addCleanup(self.temp.cleanup)
+        self.cache = Path(self.temp.name) / "iterm.json"
+        p = patch.object(ts, "_iterm_cache_path", lambda: self.cache)
+        p.start()
+        self.addCleanup(p.stop)
+        # HOME redirected so a test blind's enum_blind trail never pollutes the real file.
+        hp = patch.dict(os.environ, {"HOME": self.temp.name})
+        hp.start()
+        self.addCleanup(hp.stop)
+
+    def _osascript(self, *, rc, stdout=""):
+        """A subprocess.run stub that answers ONLY the osascript enumeration and
+        records how many times it was called (to prove the herd was collapsed)."""
+        self.calls = 0
+        real = subprocess.run
+
+        def fake(cmd, *a, **k):
+            if cmd[:1] == ["osascript"]:
+                self.calls += 1
+                return subprocess.CompletedProcess(cmd, rc, stdout, "")
+            return real(cmd, *a, **k)
+        return patch.object(ts.subprocess, "run", fake)
+
+    def test_success_enumerates_once_then_siblings_hit_the_cache(self):
+        out = "/dev/ttys010\t🟢 one\n/dev/ttys011\t🟣 two\n"
+        with self._osascript(rc=0, stdout=out):
+            first, api1 = ts.iterm_sessions()
+            second, api2 = ts.iterm_sessions()
+            third, api3 = ts.iterm_sessions()
+        self.assertEqual(first, {"ttys010": "🟢 one", "ttys011": "🟣 two"})
+        self.assertEqual((api1, api2, api3), ("available", "cached", "cached"))
+        self.assertEqual(second, first)
+        # osascript ran exactly ONCE for three enumerations -- the herd collapsed.
+        self.assertEqual(self.calls, 1)
+
+    def test_fresh_forces_a_live_enumeration_past_the_cache(self):
+        with self._osascript(rc=0, stdout="/dev/ttys010\t🟢 one\n"):
+            ts.iterm_sessions()                       # warms cache
+            _, api = ts.iterm_sessions(fresh=True)    # must bypass it
+        self.assertEqual(api, "available")
+        self.assertEqual(self.calls, 2)
+
+    def test_blind_falls_back_to_the_cached_map_not_green(self):
+        # Seed a real recent map, then a live enumeration goes blind (rc 1).
+        with self._osascript(rc=0, stdout="/dev/ttys010\t🟢 one\n"):
+            ts.iterm_sessions()
+        with patch.object(ts, "_ITERM_TTL", -1):      # force past the fresh TTL
+            with self._osascript(rc=1):
+                sessions, api = ts.iterm_sessions()
+        self.assertEqual(api, "stale")
+        self.assertEqual(sessions, {"ttys010": "🟢 one"})   # REAL map, not {}
+        # A recovered blind must NOT pollute the health failure trail.
+        trail = Path(self.temp.name) / ".claude/skills/terminal-status-health/data/repaint-failures.jsonl"
+        self.assertFalse(trail.exists(),
+                         "a blind recovered from cache must not record enum_blind")
+
+    def test_truly_blind_with_no_cache_records_enum_blind_and_is_unavailable(self):
+        # NEGATIVE / fault-injection (CLAUDE.md TK-11431 amendment 3): with NO usable
+        # cache the detector MUST still fire -- record enum_blind + return unavailable.
+        with self._osascript(rc=1):
+            sessions, api = ts.iterm_sessions()
+        self.assertEqual((sessions, api), ({}, "unavailable"))
+        trail = Path(self.temp.name) / ".claude/skills/terminal-status-health/data/repaint-failures.jsonl"
+        self.assertTrue(trail.exists(), "a genuine blind MUST record enum_blind")
+        rec = json.loads(trail.read_text().strip().splitlines()[-1])
+        self.assertEqual(rec["rc"], "enum_blind")
+
+    def test_stale_map_past_the_bound_is_not_trusted(self):
+        with self._osascript(rc=0, stdout="/dev/ttys010\t🟢 one\n"):
+            ts.iterm_sessions()
+        # Both the fresh TTL and the stale bound are in the past -> no fallback.
+        with patch.object(ts, "_ITERM_TTL", -1), patch.object(ts, "_ITERM_STALE_MAX", -1):
+            with self._osascript(rc=1):
+                sessions, api = ts.iterm_sessions()
+        self.assertEqual((sessions, api), ({}, "unavailable"))
+
+    def test_empty_successful_enum_is_not_cached_so_siblings_re_enumerate(self):
+        # rc 0 but zero sessions is a racey/partial answer; caching it would poison
+        # siblings with `cached {}` for a whole TTL and record no enum_blind.
+        with self._osascript(rc=0, stdout=""):
+            first, api1 = ts.iterm_sessions()
+            second, api2 = ts.iterm_sessions()
+        self.assertEqual((first, api1), ({}, "available"))
+        # The cache was NOT written, so the sibling enumerates again (not "cached").
+        self.assertFalse(self.cache.exists())
+        self.assertEqual((second, api2), ({}, "available"))
+        self.assertEqual(self.calls, 2)
+
+    def test_another_users_cache_file_is_never_trusted(self):
+        with self._osascript(rc=0, stdout="/dev/ttys010\t🟢 one\n"):
+            ts.iterm_sessions()
+        # Simulate a file owned by another uid: _iterm_cache_get rejects it.
+        real_uid = os.getuid()
+        with patch.object(ts.os, "getuid", lambda: real_uid + 99999):
+            self.assertIsNone(ts._iterm_cache_get(ts._ITERM_TTL))
+
+
 if __name__ == "__main__":
     unittest.main(verbosity=2)

← 56c764a dot.sh: auto-place window into its colour column on any succ  ·  back to Terminal Status  ·  TK-11831: single-flight the iTerm enumeration so a cold herd 9e5ef9e →