← 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
M terminal_status.pyM test_terminal_status.py
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 →