Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
3fa451c16This file is pinned to the selected release. Download the original to open it in your design tool.
#!/usr/bin/env python3
"""Local Codex builder-insights engine.
Reads local Codex session transcripts (~/.codex/sessions/**/*.jsonl)
and derives a "builder profile" — peak coding hour, archetype, top model, token
economy, steering style, parallelization and velocity — entirely on-device.
Privacy-first: everything is computed on-device and nothing leaves the machine.
Use `--json` to dump the computed profile, or `--display` to rotate the insight
cards onto a paired Thinking Desk display.
Stdlib only — no pip install.
"""
import argparse
import glob
import json
import os
import re
import sys
import time
from collections import Counter
from datetime import datetime, timezone
import device
CODEX_HOME = os.path.expanduser(os.environ.get("CODEX_HOME", "~/.codex"))
SESSIONS_DIR = os.path.join(CODEX_HOME, "sessions")
CACHE_PATH = os.path.join(device.CONFIG_DIR, "autonomous-lcd-codex-insights.json")
INDEX_PATH = os.path.join(device.CONFIG_DIR, "autonomous-lcd-codex-insights.idx")
CACHE_TTL = 3600 # recompute the profile at most once an hour
CARD_DELAY = float(os.environ.get("AUTONOMOUS_LCD_CARD_DELAY", "6"))
# Cold-cache analyze() runs on the 60s Stop hook. Bound the scan so a large
# ~/.codex/sessions tree cannot blow the timeout.
SESSION_MAX_AGE_SECONDS = 90 * 24 * 3600
SESSION_MAX_FILES = 120
EDIT_TOOLS = {"Edit", "Write", "NotebookEdit", "MultiEdit", "apply_patch"}
# short, corrective prompts that signal a "Director" steering style
CORRECTION_RE = re.compile(
r"^\s*(no|nope|stop|wait|undo|revert|fix|don'?t|wrong|again|"
r"actually|instead|not that|go back|khong|sai|lam lai)\b",
re.IGNORECASE,
)
# polite prompts — "thanks", "thank you", "thx", "ty", "cảm ơn"
THANKS_RE = re.compile(
r"\b(thanks|thank you|thank u|thx|tysm|cz\b|cam on|cảm ơn)\b|\bty\b",
re.IGNORECASE,
)
# --------------------------------------------------------------------------- #
# Transcript parsing
# --------------------------------------------------------------------------- #
def iter_sessions():
"""Yield recent local Codex JSONL session transcripts, newest first.
A cold cache on the Stop hook must finish inside the 60s timeout, so this
skips files older than SESSION_MAX_AGE_SECONDS (~90 days) and caps the
number of files at SESSION_MAX_FILES.
"""
pattern = os.path.join(SESSIONS_DIR, "**", "*.jsonl")
cutoff = time.time() - SESSION_MAX_AGE_SECONDS
ranked = []
for path in glob.glob(pattern, recursive=True):
try:
mtime = os.path.getmtime(path)
except OSError:
continue
if mtime >= cutoff:
ranked.append((mtime, path))
ranked.sort(reverse=True)
for _, path in ranked[:SESSION_MAX_FILES]:
yield path
def parse_ts(s):
if not s:
return None
try:
return datetime.fromisoformat(s.replace("Z", "+00:00"))
except Exception:
return None
def project_label(cwd):
"""Turn a session working directory into a short display label."""
if not cwd:
return "unknown"
return (os.path.basename(str(cwd).rstrip(os.sep)) or str(cwd))[:18]
def _record_prompt(content, ts, label, stats, by_project, hours, active_days,
phrases, crash_out):
if not isinstance(content, str) or not content.strip():
return crash_out
stats["user_prompts"] += 1
by_project[label] += 1
words = content.split()
stats["prompt_words"] += len(words)
if len(words) < 10:
stats["short_prompts"] += 1
if CORRECTION_RE.match(content) and len(words) <= 12:
stats["corrections"] += 1
if THANKS_RE.search(content):
stats["thanks"] += 1
if 2 <= len(words) <= 6:
phrases[" ".join(content.lower().split())] += 1
stripped = " ".join(content.split())
if (3 <= len(words) <= 20 and len(stripped) <= 120
and not any(ch in stripped for ch in "=/{};_<>|")):
letters = [char for char in stripped if char.isalpha()]
if len(letters) >= 8:
upper = sum(1 for char in letters if char.isupper())
if upper / len(letters) >= 0.7 and len(letters) > crash_out["letters"]:
crash_out = {"text": stripped, "letters": len(letters)}
if ts:
hours[ts.astimezone().hour] += 1
active_days.add(ts.astimezone().date().isoformat())
return crash_out
def analyze():
stats = {
"sessions": 0,
"user_prompts": 0,
"assistant_turns": 0,
"prompt_words": 0,
"corrections": 0,
"tool_calls": 0,
"multi_tool_turns": 0,
"edits": 0,
"tok_input": 0,
"tok_output": 0,
"tok_cache_read": 0,
"tok_cache_creation": 0,
"thanks": 0,
"short_prompts": 0, # prompts under 10 words (concise-ness)
}
models = Counter()
tools = Counter()
by_project = Counter()
hours = Counter() # local hour-of-day -> prompt count
active_days = set() # local date strings
phrases = Counter() # normalized short prompts -> count (go-to prompt)
crash_out = {"text": None, "letters": 0} # loudest ALL-CAPS rant
first_ts = None
last_ts = None
for path in iter_sessions():
stats["sessions"] += 1
label = "unknown"
latest_usage = None
tools_in_turn = 0
try:
fh = open(path, "r", encoding="utf-8")
except OSError:
continue
with fh:
for line in fh:
try:
d = json.loads(line)
except Exception:
continue
typ = d.get("type")
payload = d.get("payload") if isinstance(d.get("payload"), dict) else {}
ts = parse_ts(d.get("timestamp"))
if ts:
if first_ts is None or ts < first_ts:
first_ts = ts
if last_ts is None or ts > last_ts:
last_ts = ts
if typ == "session_meta":
label = project_label(payload.get("cwd"))
elif typ == "turn_context":
if tools_in_turn >= 2:
stats["multi_tool_turns"] += 1
tools_in_turn = 0
stats["assistant_turns"] += 1
if payload.get("model"):
models[payload["model"]] += 1
elif typ == "event_msg" and payload.get("type") == "user_message":
crash_out = _record_prompt(
payload.get("message"), ts, label, stats, by_project,
hours, active_days, phrases, crash_out,
)
elif typ == "event_msg" and payload.get("type") == "token_count":
latest_usage = (payload.get("info") or {}).get("total_token_usage")
elif typ == "response_item" and payload.get("type") in {
"function_call", "custom_tool_call", "local_shell_call",
}:
name = payload.get("name") or payload.get("tool_name") or payload.get("type")
stats["tool_calls"] += 1
tools_in_turn += 1
tools[name] += 1
if name in EDIT_TOOLS:
stats["edits"] += 1
if tools_in_turn >= 2:
stats["multi_tool_turns"] += 1
if isinstance(latest_usage, dict):
stats["tok_input"] += latest_usage.get("input_tokens", 0) or 0
stats["tok_output"] += latest_usage.get("output_tokens", 0) or 0
stats["tok_cache_read"] += latest_usage.get("cached_input_tokens", 0) or 0
stats["tok_cache_creation"] += latest_usage.get("cache_write_input_tokens", 0) or 0
return _derive(stats, models, tools, by_project, hours, active_days,
phrases, crash_out, first_ts, last_ts)
# --------------------------------------------------------------------------- #
# Derived metrics + archetype
# --------------------------------------------------------------------------- #
def _streak(active_days):
"""Consecutive days up to today (local)."""
if not active_days:
return 0
days = sorted(active_days, reverse=True)
today = datetime.now().astimezone().date()
from datetime import date
cur = date.fromisoformat(days[0])
# streak only counts if the most recent active day is today or yesterday
if (today - cur).days > 1:
return 0
streak = 1
prev = cur
for d in days[1:]:
dd = date.fromisoformat(d)
if (prev - dd).days == 1:
streak += 1
prev = dd
elif (prev - dd).days == 0:
continue
else:
break
return streak
def _archetype(peak_hour, corr_ratio, avg_tools, avg_words):
"""Single headline label summarising the builder."""
if peak_hour is not None and (peak_hour >= 22 or peak_hour <= 4):
return "Night Owl"
if peak_hour is not None and 5 <= peak_hour <= 8:
return "Early Bird"
if avg_tools >= 2.2:
return "Velocity Machine"
if avg_words >= 40:
return "The Architect"
if corr_ratio >= 0.18:
return "Quality Guardian"
return "Steady Builder"
def _derive(stats, models, tools, by_project, hours, active_days,
phrases, crash_out, first_ts, last_ts):
prompts = max(stats["user_prompts"], 1)
turns = max(stats["assistant_turns"], 1)
peak_hour = hours.most_common(1)[0][0] if hours else None
total_input = stats["tok_input"]
cache_hit = (stats["tok_cache_read"] / total_input * 100) if total_input else 0
corr_ratio = stats["corrections"] / prompts
avg_tools = stats["tool_calls"] / turns
avg_words = stats["prompt_words"] / prompts
short_pct = stats["short_prompts"] / prompts * 100
# go-to prompt: most repeated short phrase (must recur at least twice)
goto = next(((p, n) for p, n in phrases.most_common(20) if n >= 2), (None, 0))
top_model = models.most_common(1)[0][0] if models else "n/a"
model_total = sum(models.values()) or 1
top_model_pct = (models.most_common(1)[0][1] / model_total * 100) if models else 0
return {
"generated_at": datetime.now(timezone.utc).isoformat(),
"span": {
"first": first_ts.isoformat() if first_ts else None,
"last": last_ts.isoformat() if last_ts else None,
"active_days": len(active_days),
"streak": _streak(active_days),
},
"activity": {
"sessions": stats["sessions"],
"prompts": stats["user_prompts"],
"assistant_turns": stats["assistant_turns"],
"peak_hour": peak_hour,
"hour_histogram": dict(sorted(hours.items())),
},
"models": {
"top": short_model(top_model),
"top_raw": top_model,
"top_pct": round(top_model_pct, 1),
"breakdown": {short_model(k): v for k, v in models.most_common(5)},
},
"tokens": {
"input": stats["tok_input"],
"output": stats["tok_output"],
"cache_read": stats["tok_cache_read"],
"cache_creation": stats["tok_cache_creation"],
"cache_hit_pct": round(cache_hit, 1),
},
"steering": {
"avg_prompt_words": round(avg_words, 1),
"correction_ratio": round(corr_ratio, 3),
"short_prompt_pct": round(short_pct, 1),
},
"habits": {
"goto_prompt": goto[0],
"goto_count": goto[1],
"thanks": stats["thanks"],
"crash_out": crash_out["text"],
},
"velocity": {
"tool_calls": stats["tool_calls"],
"avg_tools_per_turn": round(avg_tools, 2),
"multi_tool_turns": stats["multi_tool_turns"],
"edits": stats["edits"],
"top_tools": dict(tools.most_common(5)),
},
"top_projects": dict(by_project.most_common(5)),
"archetype": _archetype(peak_hour, corr_ratio, avg_tools, avg_words),
}
def short_model(m):
if not m or m == "n/a":
return "n/a"
m = m.replace("claude-", "").replace("<synthetic>", "synthetic")
# opus-4-7 -> Opus 4.7
mm = re.match(r"(opus|sonnet|haiku)-(\d+)-(\d+)", m)
if mm:
return f"{mm.group(1).title()} {mm.group(2)}.{mm.group(3)}"
if m.startswith("gpt-"):
return m.upper().replace("GPT-", "GPT ")
return m
# --------------------------------------------------------------------------- #
# Display cards
# --------------------------------------------------------------------------- #
def fmt_tokens(n):
if n >= 1_000_000:
return f"{n/1_000_000:.1f}M"
if n >= 1_000:
return f"{n/1_000:.0f}K"
return str(n)
def fmt_hour(h):
if h is None:
return "n/a"
suffix = "am" if h < 12 else "pm"
hh = h % 12 or 12
return f"{hh}{suffix}"
# The device font is PROPORTIONAL and renders wider than a naive 6px/char — in
# practice ~7px per character at size 1, scaling with the size multiplier. The
# content viewport is 220px wide, so size text against a conservative budget and
# keep big titles short (≤ ~8 chars) to avoid clipping at the screen edge.
CHAR_W = 7
NOTIFY_SOUND = 2 # lift_chime — gentle reveal cue on the first (archetype) card
def fit_size(text, box_w, max_size=4, min_size=1):
n = max(len(text), 1)
for s in range(max_size, min_size - 1, -1):
if n * CHAR_W * s <= box_w:
return s
return min_size
def ellipsize(text, box_w, size):
"""Trim text (with a trailing ellipsis) so it fits box_w at the given size."""
max_chars = max(box_w // (CHAR_W * size), 1)
if len(text) <= max_chars:
return text
if max_chars <= 1:
return text[:max_chars]
return text[: max_chars - 1] + "…"
ACCENT = "#d4845a"
CREAM = "#e8dcc8"
BLUE = "#7eb8da"
MUTE = "#9a9488"
GREEN = "#9bad67"
RED = "#c0392b"
def _card(title, content, sub, c_color=ACCENT, c_max=3, sound=0):
"""Standard 3-line card: short title, auto-fit headline, small footnote.
Headline is sized against a conservative 185px budget; if even size 1 is too
wide it gets ellipsized. Sub line is trimmed to fit at size 1.
"""
size = fit_size(content, 185, c_max)
return {
"play_sound": sound,
"items": [
{"type": "text", "text": ellipsize(title, 210, 2), "x": 0, "y": 8, "width": 220, "align": "center", "size": fit_size(title, 200, 2), "color": BLUE},
{"type": "text", "text": ellipsize(content, 200, size), "x": 0, "y": 36, "width": 220, "align": "center", "size": size, "color": c_color},
{"type": "text", "text": ellipsize(sub, 210, 1), "x": 0, "y": 80, "width": 220, "align": "center", "size": 1, "color": MUTE},
],
}
def _owl_label(h):
if h is None:
return "builder"
if h >= 22 or h <= 4:
return "Night owl"
if 5 <= h <= 8:
return "Early bird"
if 9 <= h <= 11:
return "Morning"
if 12 <= h <= 17:
return "Afternoon"
return "Evening"
def build_cards(p):
"""Return the rotation of card payloads, derived from the live profile.
Always shows the 5 stable cards; appends the go-to-prompt and crash-out
cards only when there's real data for them (no blank cards)."""
span, act, hab = p["span"], p["activity"], p.get("habits", {})
cards = []
# 1. Archetype (first card carries the reveal sound)
cards.append(_card(
"You are", p["archetype"],
f"{span['streak']}d streak · {span['active_days']} days",
ACCENT, 3, sound=NOTIFY_SOUND))
# 2. Peak hour
cards.append(_card(
"Peak hour", fmt_hour(act["peak_hour"]),
f"{_owl_label(act['peak_hour'])} · {act['sessions']} sess",
CREAM, 4))
# 3. Top model
cards.append(_card(
"Top model", p["models"]["top"],
f"{int(p['models']['top_pct'])}% of turns", ACCENT, 3))
# 4. Prompt style (dynamic: detailed vs concise)
s = p["steering"]
if s["avg_prompt_words"] >= 40:
cards.append(_card("Your style", "Detailed",
f"~{int(s['avg_prompt_words'])} words/prompt", CREAM, 3))
else:
cards.append(_card("Your style", "Concise",
f"{int(s['short_prompt_pct'])}% under 10 words", CREAM, 3))
# 5. Manners (thanks count — always available, even if 0)
thanks = hab.get("thanks", 0)
polite_sub = "robots remember you" if thanks >= 10 else \
("a quiet nod" if thanks else "straight to business")
cards.append(_card("Manners", f"{thanks} thanks", polite_sub, GREEN, 3))
# 6. Go-to prompt (only if a phrase recurs)
if hab.get("goto_prompt"):
cards.append(_card("Most used", f'"{hab["goto_prompt"]}"',
f"{hab['goto_count']}× across sessions", ACCENT, 2))
# 7. Crash out (only if a loud ALL-CAPS rant exists)
if hab.get("crash_out"):
cards.append(_card("Crash out", hab["crash_out"],
"caps lock engaged", RED, 1))
return cards
# --------------------------------------------------------------------------- #
# Device send (mirrors on-stop-done.py)
# --------------------------------------------------------------------------- #
def load_device():
cfg = device.load_config()
dev = device.get_device(cfg)
return (cfg, dev) if dev else (cfg, None)
def display(profile, once=False):
cfg, dev = load_device()
if not dev:
print("No paired display. Run 'pair my display' first.", file=sys.stderr)
return 1
cards = build_cards(profile)
if once:
cards = cards[:1]
for i, payload in enumerate(cards):
try:
if not device.send_with_reconnect(cfg, dev, payload):
raise OSError("display unreachable")
except Exception as e:
print(f"send failed: {e}", file=sys.stderr)
return 1
if i < len(cards) - 1:
time.sleep(CARD_DELAY)
return 0
def display_card(profile, index):
"""Send exactly one card (by rotation index). Used by the daemon."""
cfg, dev = load_device()
if not dev:
return 1
cards = build_cards(profile)
if not cards:
return 1
try:
if not device.send_with_reconnect(cfg, dev, cards[index % len(cards)]):
raise OSError("display unreachable")
except Exception as e:
print(f"send failed: {e}", file=sys.stderr)
return 1
return 0
def rotate_one(cfg=None, dev=None):
"""Send the next ambient insight card and advance the private local index."""
cfg = cfg or device.load_config()
dev = dev or device.get_device(cfg)
if not dev or not cfg.get("show_insights", True):
return 1
try:
with open(INDEX_PATH, "r", encoding="utf-8") as handle:
index = int(handle.read().strip())
except (OSError, TypeError, ValueError):
index = 0
cards = build_cards(get_profile())
if not cards or not device.send_with_reconnect(cfg, dev, cards[index % len(cards)]):
return 1
try:
os.makedirs(device.CONFIG_DIR, mode=0o700, exist_ok=True)
with open(INDEX_PATH, "w", encoding="utf-8") as handle:
handle.write(str(index + 1))
os.chmod(INDEX_PATH, 0o600)
except OSError:
pass
return 0
# --------------------------------------------------------------------------- #
# Cached profile (avoid re-scanning every transcript on each daemon tick)
# --------------------------------------------------------------------------- #
def get_profile(force=False):
if not force and os.path.exists(CACHE_PATH):
try:
age = time.time() - os.path.getmtime(CACHE_PATH)
if age < CACHE_TTL:
with open(CACHE_PATH) as f:
return json.load(f)
except Exception:
pass
profile = analyze()
try:
device.atomic_write_json(CACHE_PATH, profile)
except Exception:
pass
return profile
# --------------------------------------------------------------------------- #
def render_text(p):
s = p["span"]
a = p["activity"]
lines = [
f" Archetype : {p['archetype']}",
f" Streak : {s['streak']}d ({s['active_days']} active days)",
f" Peak hour : {fmt_hour(a['peak_hour'])}",
f" Sessions : {a['sessions']} Prompts: {a['prompts']}",
f" Top model : {p['models']['top']}",
f" Tokens : {fmt_tokens(p['tokens']['output'])} out, "
f"{p['tokens']['cache_hit_pct']}% cache hit",
f" Style : {p['steering']['avg_prompt_words']} words/prompt, "
f"{p['velocity']['avg_tools_per_turn']} tools/turn",
f" Projects : " + ", ".join(p["top_projects"].keys()),
]
hab = p.get("habits", {})
if hab.get("goto_prompt"):
lines.append(f" Go-to : \"{hab['goto_prompt']}\" ({hab['goto_count']}x)")
lines.append(f" Manners : {hab.get('thanks', 0)} thanks")
if hab.get("crash_out"):
lines.append(f" Crash out : {hab['crash_out']}")
return "Builder Profile (local)\n" + "\n".join(lines)
def main():
ap = argparse.ArgumentParser(description="Local Codex builder insights")
ap.add_argument("--json", action="store_true", help="dump full profile as JSON")
ap.add_argument("--display", action="store_true", help="rotate cards on paired display")
ap.add_argument("--once", action="store_true", help="with --display, send only the first card")
ap.add_argument("--card", type=int, metavar="N", help="send only card N (mod rotation length)")
ap.add_argument("--fresh", action="store_true", help="force a recompute, ignore cache")
args = ap.parse_args()
profile = analyze() if args.fresh else get_profile(force=args.fresh)
if args.card is not None:
return display_card(profile, args.card)
if args.display:
return display(profile, once=args.once)
if args.json:
print(json.dumps(profile, indent=2))
else:
print(render_text(profile))
return 0
if __name__ == "__main__":
sys.exit(main())