Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ The `show_day_stats` **Config** knob (`YAS_SHOW_DAY_STATS`, `[tokens].show_day_s
The time remaining before this session's prompt cache expires, shown as `<cache-glyph> <MM:SS>` (e.g. `03:07`, `00:42`), rolling to `H:MM:SS` at or above 3600 s, in its own vsep-delimited section on the path/model row, between the rate-limit helper and the model pill. The anchor is the `timestamp` of the most recent transcript line that touched the prompt cache (`cache_read_input_tokens > 0` or `cache_creation_input_tokens > 0`); `remaining = ttl − (now − anchor)`. The **Cache TTL** is 300 s by default, 3600 s when the anchor line wrote to the 1-hour ephemeral tier (`cache_creation.ephemeral_1h_input_tokens > 0`). Re-derived from the transcript every render against the frozen `now` — no per-session state file. The figure is coloured by `fill_colour(elapsed_pct)` where `elapsed_pct = 100 − round(remaining·100/ttl)`, so it runs green when fresh → red near expiry (the same safe/warn/alert ladder as the rate-limit percentages). The whole section — divider included — is hidden when there has never been a cache event, when `remaining ≤ 0` (expired), or when the row is too narrow to fit it (it sheds first, before the path truncates).
_Avoid_: "Cache Read" (that is the `cache_read_input_tokens` token figure in the tokens row — a token count, not a time), and "cache TTL" as the *displayed* term (the **Cache TTL** is the 300/3600 s lifetime constant; the **Cache Countdown** is the live remaining time derived from it).

**Transcript Parse Cache**:
A performance-only persistent store of parsed transcript results, entirely invisible to the statusline output. When enabled (controlled by the `transcript_cache` **Config** knob, `YAS_TRANSCRIPT_CACHE`, or `[cache].transcript_cache` in `yas.toml`; default on), the application layer loads the cache at the start of a render and saves it once after the render completes. The cache deduplicates work across renders by keying on `(transcript_path, mtime, size)` — when a transcript has not changed since the last render, the cached parse result is reused and the file is not re-read. Entries are automatically expired when a transcript is modified or deleted, and pruned on save once their transcript no longer exists or they have gone unseen past the retention horizon. One JSON file per session, at `~/.claude/yas-cache/transcripts.<session_id>.json`, carrying a version stamp that discards the whole file on any shape change. Every stored value is re-derivable, so a missing, stale or corrupt cache costs only a full re-parse. No rendering, no timestamp, no output — purely backend optimization.
_Avoid_: "Cache Read" (that is the `cache_read_input_tokens` token figure — a billed token count) and "Cache Countdown" (that is the time remaining before the prompt cache expires — a per-session TTL tied to the conversation, not a parse-result store). All three contain the word "cache"; the **Transcript Parse Cache** is unrelated to both — it is a performance mechanism keyed on file identity, not a token count or countdown.

### Context window

**Context Window Size**:
Expand Down
12 changes: 8 additions & 4 deletions claude/yas/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
config_path, session_payload_path, sessions_dir, version_file,
)
from yas.info import SessionView
from yas.info.parsecache import TranscriptCache
from yas.layout import build_narrow, build_medium, build_wide, render_layout
from yas.renderer import Renderer
from yas.session import SessionInfo, _as_str
Expand Down Expand Up @@ -42,15 +43,16 @@ def resolve_theme(cli_name: str | None) -> Theme:
def render(session_info: dict[str, object], width: int, *, bg_shift: str = 'warm', theme: Theme | None = None, glyph_mode: str | None = None, single_width: bool | None = None, timing: str = '') -> str:
if width < MIN_WIDTH:
return ''
session = SessionInfo.from_dict(session_info)
r = Renderer(bg_shift=bg_shift, theme=theme)
cfg = Config.load()
session = SessionInfo.from_dict(session_info)
r = Renderer(bg_shift=bg_shift, theme=theme)
cfg = Config.load()
parse_cache = TranscriptCache.load(session.session_id) if cfg.transcript_cache else None
if glyph_mode is None:
glyph_mode = cfg.glyph_mode
if single_width is None:
single_width = cfg.single_width
soft_limit = cfg.soft_limit_for(session.model.id, session.model.display_name)
view = SessionView(session, cfg)
view = SessionView(session, cfg, cache=parse_cache)
if width < NARROW_WIDTH:
spec = build_narrow(view, width, r, soft_limit)
elif width < MEDIUM_WIDTH:
Expand All @@ -62,6 +64,8 @@ def render(session_info: dict[str, object], width: int, *, bg_shift: str = 'warm
# grey→muted-grey gradient), preceded by the previous run's wall-clock
# when the show_render_time knob supplies it (`…47.2ms v0.6.2──╯`).
out = '\n'.join(render_layout(spec, r, timing, f'v{VERSION}'))
if parse_cache is not None:
parse_cache.save()
return apply_glyphs(out, glyph_mode, single_width)


Expand Down
19 changes: 15 additions & 4 deletions claude/yas/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
DEFAULT_THEME,
DEFAULT_SHOW_DAY_STATS,
DEFAULT_SHOW_TOOL_USES,
DEFAULT_TRANSCRIPT_CACHE,
config_path,
)
from yas.themes import THEMES
Expand Down Expand Up @@ -352,7 +353,7 @@ class Config:
'token_window', 'theme', 'bg_shift', 'glyph_mode', 'single_width',
'show_day_stats', 'context_state', 'context_labels', 'context_thresholds',
'show_render_time', 'show_tool_uses', 'soft_limit_models', 'openspec_scan_depth',
'show_icons', 'errors', 'debug_lines',
'show_icons', 'transcript_cache', 'errors', 'debug_lines',
)

max_width: int
Expand All @@ -373,7 +374,8 @@ class Config:
show_tool_uses: bool
soft_limit_models: tuple[tuple[str, int], ...]
openspec_scan_depth: int
show_icons: bool
show_icons: bool
transcript_cache: bool
errors: tuple[str, ...]
debug_lines: tuple[str, ...]

Expand All @@ -397,7 +399,8 @@ def __init__(
show_tool_uses: bool = DEFAULT_SHOW_TOOL_USES,
soft_limit_models: tuple[tuple[str, int], ...] = (),
openspec_scan_depth: int = DEFAULT_OPENSPEC_SCAN_DEPTH,
show_icons: bool = True,
show_icons: bool = True,
transcript_cache: bool = DEFAULT_TRANSCRIPT_CACHE,
errors: tuple[str, ...] = (),
debug_lines: tuple[str, ...] = (),
) -> None:
Expand All @@ -421,6 +424,7 @@ def __init__(
s(self, 'soft_limit_models', soft_limit_models)
s(self, 'openspec_scan_depth', openspec_scan_depth)
s(self, 'show_icons', show_icons)
s(self, 'transcript_cache', transcript_cache)
s(self, 'errors', errors)
s(self, 'debug_lines', debug_lines)

Expand All @@ -440,7 +444,7 @@ def __repr__(self) -> str:
f'show_render_time={self.show_render_time}, show_tool_uses={self.show_tool_uses}, '
f'soft_limit_models={self.soft_limit_models!r}, '
f'openspec_scan_depth={self.openspec_scan_depth}, '
f'show_icons={self.show_icons}, '
f'show_icons={self.show_icons}, transcript_cache={self.transcript_cache}, '
f'errors={self.errors!r}, debug_lines={self.debug_lines!r})')

@classmethod
Expand Down Expand Up @@ -473,6 +477,7 @@ def _table_in(table: dict[str, object], name: str) -> dict[str, object]:
layout, tokens, appearance = _table('layout'), _table('tokens'), _table('appearance')
context = _table('context')
openspec = _table('openspec')
cache = _table('cache')
glyphs = _table_in(appearance, 'glyphs')
cli = _parse_argv(argv) if argv is not None else {}

Expand Down Expand Up @@ -564,6 +569,11 @@ def cli_src(name: str) -> list[tuple[str, object]]:
_env_sources(env, 'YAS_OPENSPEC_SCAN_DEPTH') + toml_src(openspec, 'scan_depth'),
_parse_nonneg_int, DEFAULT_OPENSPEC_SCAN_DEPTH, errors, debug)

transcript_cache = _resolve(
'transcript_cache',
_env_sources(env, 'YAS_TRANSCRIPT_CACHE') + toml_src(cache, 'transcript_cache'),
_parse_bool, DEFAULT_TRANSCRIPT_CACHE, errors, debug)

soft_limit_models = _parse_models(tokens.get('model'), errors, debug)

return cls(
Expand All @@ -586,6 +596,7 @@ def cli_src(name: str) -> list[tuple[str, object]]:
soft_limit_models=tuple(soft_limit_models),
openspec_scan_depth=openspec_scan_depth,
show_icons=show_icons,
transcript_cache=transcript_cache,
errors=tuple(errors),
debug_lines=tuple(debug),
)
Expand Down
10 changes: 9 additions & 1 deletion claude/yas/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

# Keep in sync with pyproject.toml's [project] version — pyproject isn't
# shipped with the runtime copy under ~/.claude, so the value lives here too.
VERSION = '0.8.0'
VERSION = '0.8.1'
# Bumped by any future on-disk relayout under yas/; stamped into
# state/version.json by yas.migrate so a future migration can detect and
# convert an older layout.
Expand Down Expand Up @@ -66,6 +66,10 @@ def toml_cache_path() -> Path:
return cache_dir() / 'config.toml.cache'


def transcript_cache_path(session_id: str) -> Path:
return cache_dir() / f'transcripts.{session_id}.json'


def tokens_log() -> Path:
return runtime_dir() / 'tokens.log'

Expand Down Expand Up @@ -118,6 +122,10 @@ def settings_path() -> Path:
DEFAULT_CONTEXT_STATE = False
DEFAULT_CONTEXT_LABELS: tuple[str, ...] = ('Smart', 'Coasting', 'Foggy', 'Cooked', 'Dumb')
DEFAULT_CONTEXT_THRESHOLDS: tuple[int, ...] = (25, 50, 70, 90)
TRANSCRIPT_CACHE_VERSION = 1
TRANSCRIPT_CACHE_KEEP_SECONDS = 86400.0 # 24 h — comfortably beyond ABANDONED_HORIZON_SECONDS = 1800
TRANSCRIPT_CACHE_SUBKEY_MAX = 4 # max sub-keys retained per transcript per result kind
DEFAULT_TRANSCRIPT_CACHE = True
NARROW_WIDTH = 55
MEDIUM_WIDTH = 80
# Box width at/above which the wide layout's workflow cohort pairs agents into
Expand Down
20 changes: 13 additions & 7 deletions claude/yas/info/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

All I/O is deferred to first access via @cached_property. Callers
construct a SessionView and read only the fields they need; unread
fields never touch the filesystem.
fields never touch the filesystem. SessionView may hold a loaded
transcript parse cache for deduplication but never writes it.
"""

from __future__ import annotations
Expand All @@ -22,6 +23,7 @@
from yas.info.transcript import TranscriptUsage
from yas.info.toolcounts import ToolCounts
from yas.info.clear import read_clear_epoch
from yas.info.parsecache import TranscriptCache


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -76,10 +78,11 @@ def _fmt_elapsed(mtime: float | None, now: float) -> str:
# ---------------------------------------------------------------------------

class SessionView:
def __init__(self, session: SessionInfo, cfg: Config, now: float | None = None) -> None:
self.session = session
self.cfg = cfg
self.now = time.time() if now is None else now
def __init__(self, session: SessionInfo, cfg: Config, now: float | None = None, cache: TranscriptCache | None = None) -> None:
self.session = session
self.cfg = cfg
self.now = time.time() if now is None else now
self.parse_cache = cache

# ------------------------------------------------------------------
# Leaf readers — each delegates to its existing classmethod
Expand All @@ -98,6 +101,7 @@ def subagents(self) -> RunningSubagents:
return RunningSubagents.from_session(
self.session.session_id,
self.session.workspace.project_dir,
cache=self.parse_cache,
)

@cached_property
Expand Down Expand Up @@ -129,13 +133,15 @@ def tool_counts(self) -> ToolCounts:
- per_agent breakdown of lines_read and lines_changed for each subagent.

Reopens the main transcript and each subagent transcript — no I/O beyond
the files already scanned this render. Lazy: a narrow/medium render that
never reads this never pays for the aggregation.
the files already scanned this render. A cached, unchanged transcript is
not reopened at all. Lazy: a narrow/medium render that never reads this
never pays for the aggregation.
"""
return ToolCounts.gather(
self.session.transcript_path,
self.subagents.subagents,
self.clear_epoch,
cache=self.parse_cache,
)

# ------------------------------------------------------------------
Expand Down
Loading