v0.8.7: status line renders on MobaXterm — gate on turn count not data presence

Root cause: render_status_line suppressed the OAuth line whenever ctx_used,
5h_util, and 7d_util were ALL empty. On a rate-limited session ctx is never
recorded (the error path returns before _record_ctx_used) and pre-v0.8.5 the
unified-* headers weren't captured on errors — so all three stayed empty turn
after turn and the line never appeared on Bryan's work-box. NOT a positioning
bug: the line is a plain printf'd dim line (no scroll-region/cursor escapes)
and is not coupled to streaming or mouse mode.

Fix: suppress only before the first turn (_LARRY_TURNS==0); thereafter always
render — empty fields show "—" placeholders, reset date fills in once headers
populate. /status now renders on demand even pre-first-turn. CR-taint sweep:
coerce_int the reset-epoch arithmetic comparisons + strip_cr the oauth-status
color case (MobaXterm CRLF would otherwise crash/blank the line).

Verify: bash -n clean; 7/7 unit tests (turn-0 suppressed, turn>=1 placeholders,
reset date when populated, renders with LARRY_NO_STREAM=1 + mouse off, survives
CR-tainted epoch, LARRY_NO_STATUS=1 still disables).

Co-Authored-By: Clover (Claude Opus 4.7) <noreply@anthropic.com>
This commit is contained in:
Bryan Johnson 2026-05-27 21:41:03 -07:00
parent 578cefcc35
commit 4a992d9668
3 changed files with 133 additions and 41 deletions

View File

@ -4,6 +4,59 @@ All notable changes to `cloverleaf-larry` / `larry-anywhere` are recorded here.
Versioning is loose-semver; bumps trigger the in-process self-update on every Versioning is loose-semver; bumps trigger the in-process self-update on every
running client via `LARRY_BASE_URL` + `MANIFEST`. running client via `LARRY_BASE_URL` + `MANIFEST`.
## v0.8.7 — 2026-05-27
Status-line render fix for MobaXterm/Cygwin (Clover). Symptom: the dim
between-turn status line (session context + rate-limit reset date) never
appeared on Bryan's MobaXterm work-box — the v0.7.1 status-line feature that
MEMORY.md flagged as a never-verified passive item.
**Root cause = suppress-when-empty gate, NOT terminal positioning.**
`render_status_line` (larry.sh) gated the OAuth arm on `ctx_used_tokens`,
`oauth_5h_utilization`, AND `oauth_7d_utilization` ALL being empty — returning
silently (rendering nothing) when so. Two facts made all three stay empty turn
after turn on Bryan's box: (1) `STATUS_ctx_used_tokens` is populated by
`_record_ctx_used`, which runs only AFTER a successful `agent_turn`; on a
`rate_limit_error` the turn returns early (larry.sh ~L3929), so ctx was never
recorded; (2) pre-v0.8.5 the `anthropic-ratelimit-unified-*` utilization
headers weren't captured on error responses. With every turn erroring, all
three gate fields were empty every turn, so the line was suppressed for the
whole session and never rendered. This was NOT a positioning bug: the status
line is a single plain `printf`'d dim line printed between turns — there is no
DECSTBM scroll-region reservation, no cursor save/restore, no absolute-row
positioning anywhere in the codebase, so MobaXterm's terminal emulation had
nothing to mis-honor. It was also NOT coupled to streaming or mouse mode.
- **Gate on turn count, not data presence.** `render_status_line` now suppresses
ONLY before the first turn has run (`_LARRY_TURNS == 0`, coerce_int-guarded);
thereafter it ALWAYS renders. Both auth-mode helpers already self-render a
`—` placeholder for any unpopulated field, so the line always shows session
context (model context window, turns, session cost) and the rate-limit reset
time fills in once a successful call — or, since v0.8.5, a captured error
response — populates the headers.
- **`/status` always renders on demand**, even before the first turn — an
explicit request bypasses the turn-0 gate. Lets Bryan verify the line renders
on MobaXterm without first completing a (possibly rate-limited) turn.
- **CR-taint hardening (same-pattern sweep).** The OAuth segment's reset-epoch
comparisons (`[ <epoch> -le <now> ]`) read `STATUS_oauth_{5h,7d}_reset_epoch`,
which come from `_header_value` (strips only the TRAILING CR). A CRLF response
on MobaXterm or a non-numeric token would have crashed the arithmetic test and
aborted the entire line. Both comparisons now `coerce_int` the epoch first;
the `STATUS_oauth_status` color-override `case` is now `strip_cr`-guarded so a
`rate_limited\r` value still matches its literal-glob arm.
- **Same-pattern sweep results:** audited every escape sequence in larry.sh +
lib/ — only color SGR, clear-screen (`\033[2J\033[H`), erase-line
(`\r\033[K`), and the opt-in mouse/bracketed-paste modes (off by default since
v0.7.5) are used; ZERO scroll-region / cursor-save-restore / absolute-cursor
sequences, so no other UI element is at risk of MobaXterm mis-rendering.
Confirmed no user-visible element is gated behind streaming (`used_stream`
only guards re-printing already-streamed text) or mouse mode.
- **Verification:** `bash -n` clean; 7/7 unit tests pass against the shipped
render functions — turn-0 suppressed; turn≥1 with empty data renders with
placeholders; reset date shown when populated; renders with `LARRY_NO_STREAM=1`
+ mouse off (Bryan's exact config); survives a CR-tainted reset epoch without
crashing; `LARRY_NO_STATUS=1` still fully disables.
## v0.8.6 — 2026-05-27 ## v0.8.6 — 2026-05-27
Work-box → Mac `headers.log` sync (tsk-2026-05-27-023, Clover headers-sync). Work-box → Mac `headers.log` sync (tsk-2026-05-27-023, Clover headers-sync).

View File

@ -1 +1 @@
0.8.6 0.8.7

119
larry.sh
View File

@ -65,7 +65,7 @@ set -o pipefail
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
# Config # Config
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
LARRY_VERSION="0.8.6" LARRY_VERSION="0.8.7"
LARRY_HOME="${LARRY_HOME:-$HOME/.larry}" LARRY_HOME="${LARRY_HOME:-$HOME/.larry}"
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
@ -2626,32 +2626,47 @@ _parse_response_headers() {
# agent_turn begins (was: above the prompt, v0.6.9v0.7.0). It now reads # agent_turn begins (was: above the prompt, v0.6.9v0.7.0). It now reads
# as a "between turns" marker summarising the just-completed turn's cost # as a "between turns" marker summarising the just-completed turn's cost
# heading into the new request. # heading into the new request.
# Honors LARRY_NO_STATUS=1. Prints nothing if we have no data yet (first #
# turn of a session). Always ends with a trailing newline so the next # Honors LARRY_NO_STATUS=1. Always ends with a trailing newline so the next
# stream lands cleanly below. # stream lands cleanly below.
#
# v0.8.7 — suppress ONLY on the true first turn (no turn has run yet,
# _LARRY_TURNS==0). After that, ALWAYS render — even if the rate-limit /
# context fields are still empty (the segments self-render "—" placeholders).
# Earlier (v0.6.9v0.8.6) the OAuth arm gated on ctx/5h/7d ALL being empty,
# which silently hid the entire line for a whole session whenever every turn
# erred (e.g. rate_limit): _record_ctx_used (which populates ctx) runs only
# AFTER a successful agent_turn, and pre-v0.8.5 the unified-* utilization
# headers weren't captured on error responses — so all three gate fields
# stayed empty turn after turn and the line never appeared on MobaXterm.
# Root cause of the "status line missing on the work-box" report.
# This is NOT a positioning bug: the line is a plain printf'd dim line printed
# between turns (no DECSTBM scroll-region / cursor-save / absolute-row escape),
# so MobaXterm's terminal emulation has nothing to mis-honor. It is also NOT
# coupled to streaming or mouse mode — the between-turn call site (main_loop)
# invokes it unconditionally regardless of LARRY_NO_STREAM / LARRY_MOUSE.
# Gate on turn count, NOT data presence, so session context (model, turns,
# cost, ctx window) always shows and the reset date fills in once a call
# populates the headers.
render_status_line() { render_status_line() {
[ "${LARRY_NO_STATUS:-0}" = "1" ] && return 0 [ "${LARRY_NO_STATUS:-0}" = "1" ] && return 0
# Pick template by auth mode. # Suppress ONLY before the first turn has run. coerce_int defends against a
# CR-tainted counter on Cygwin/MobaXterm (v0.7.5 lesson — never feed a raw
# value to `-eq`).
local _turns; _turns=$(coerce_int "$_LARRY_TURNS" 0)
[ "$_turns" -eq 0 ] && return 0
# Pick template by auth mode. Both arms self-render "—" for any field that
# has no data yet, so the line is always informative even on a fresh or
# error-only session.
case "$LARRY_AUTH_MODE" in case "$LARRY_AUTH_MODE" in
oauth) oauth) _render_status_line_oauth ;;
# Suppress if we have NO context data AND no OAuth data — first turn. apikey) _render_status_line_apikey ;;
if [ -z "$STATUS_ctx_used_tokens" ] \
&& [ -z "$STATUS_oauth_5h_utilization" ] \
&& [ -z "$STATUS_oauth_7d_utilization" ]; then
return 0
fi
_render_status_line_oauth
;;
apikey)
# Suppress only when context AND cost both absent (first turn).
if [ -z "$STATUS_ctx_used_tokens" ] && [ "$_LARRY_TURNS" -eq 0 ]; then
return 0
fi
_render_status_line_apikey
;;
*) *)
return 0 ;; # Unknown auth mode: still show the universal context segment + turns so
# the operator gets feedback rather than a blank line.
_render_status_line_apikey ;;
esac esac
} }
@ -2714,11 +2729,16 @@ _render_status_line_oauth() {
else else
five_pct="—" five_pct="—"
fi fi
if [ -n "$STATUS_oauth_5h_reset_epoch" ]; then # v0.8.7: coerce_int the reset epoch before the `-le` test. These values come
if [ "$STATUS_oauth_5h_reset_epoch" -le "$now" ]; then # from _header_value, which strips only the TRAILING CR; an embedded CR (CRLF
# response on MobaXterm) or a non-numeric token would crash `[ X -le N ]` with
# an arithmetic error and abort the whole line — same defense as `now` above.
local _5h_epoch; _5h_epoch=$(coerce_int "$STATUS_oauth_5h_reset_epoch" 0)
if [ -n "$STATUS_oauth_5h_reset_epoch" ] && [ "$_5h_epoch" -gt 0 ]; then
if [ "$_5h_epoch" -le "$now" ]; then
five_reset="— reset" five_reset="— reset"
else else
five_reset="reset $(_epoch_to_hhmm "$STATUS_oauth_5h_reset_epoch")" five_reset="reset $(_epoch_to_hhmm "$_5h_epoch")"
fi fi
else else
five_reset="reset —" five_reset="reset —"
@ -2735,19 +2755,25 @@ _render_status_line_oauth() {
else else
seven_pct="—" seven_pct="—"
fi fi
if [ -n "$STATUS_oauth_7d_reset_epoch" ]; then # v0.8.7: coerce_int the 7d reset epoch — same CR-taint / arithmetic-crash
if [ "$STATUS_oauth_7d_reset_epoch" -le "$now" ]; then # defense as the 5h segment above.
local _7d_epoch; _7d_epoch=$(coerce_int "$STATUS_oauth_7d_reset_epoch" 0)
if [ -n "$STATUS_oauth_7d_reset_epoch" ] && [ "$_7d_epoch" -gt 0 ]; then
if [ "$_7d_epoch" -le "$now" ]; then
seven_reset="— reset" seven_reset="— reset"
else else
seven_reset="reset $(_epoch_to_ddd_mmm_d "$STATUS_oauth_7d_reset_epoch")" seven_reset="reset $(_epoch_to_ddd_mmm_d "$_7d_epoch")"
fi fi
else else
seven_reset="reset —" seven_reset="reset —"
fi fi
# Status-level color override (warning → yellow, rate_limited → red wins). # Status-level color override (warning → yellow, rate_limited → red wins).
# v0.8.7: strip_cr before the case — STATUS_oauth_status comes from an API
# header and a CRLF response (MobaXterm) would leave "rate_limited\r", which
# the literal-glob case arm would never match (silent loss of the red cue).
local overall_pre="" local overall_pre=""
case "$STATUS_oauth_status" in case "$(strip_cr "$STATUS_oauth_status")" in
rate_limited) overall_pre="$C_RED" ;; rate_limited) overall_pre="$C_RED" ;;
warning) overall_pre="$C_YELLOW" ;; warning) overall_pre="$C_YELLOW" ;;
esac esac
@ -4318,13 +4344,19 @@ Multi-line input:
are not matched. Binary files and files >250 KB are skipped/truncated with are not matched. Binary files and files >250 KB are skipped/truncated with
a warning. TAB after @ autocompletes against files in cwd (fzf if installed). a warning. TAB after @ autocompletes against files in cwd (fzf if installed).
Status line (v0.6.9, repositioned v0.7.1): Status line (v0.6.9, repositioned v0.7.1, render fix v0.8.7):
A dim 1-line summary prints between turns — after you submit input and A dim 1-line summary prints between turns — after you submit input and
before larry's response begins — summarising the just-completed turn: before larry's response begins — summarising the just-completed turn:
OAuth: ─ ctx 12% (24K/200K) ─ 5h 1.8% reset 19:45 ─ 7d 73.7% reset Mon Jun 2 OAuth: ─ ctx 12% (24K/200K) ─ 5h 1.8% reset 19:45 ─ 7d 73.7% reset Mon Jun 2
API key: ─ ctx 12% (24K/200K)$0.213 session ─ 14 turns ─ API key: ─ ctx 12% (24K/200K)$0.213 session ─ 14 turns ─
Disable entirely with LARRY_NO_STATUS=1. Force re-display with /status. Disable entirely with LARRY_NO_STATUS=1. Force re-display anytime with
Suppressed automatically on the first turn (no data yet). /status (renders even before the first turn). Suppressed automatically
ONLY before the first turn has run; thereafter it always renders — fields
with no data yet show a "—" placeholder, and the rate-limit reset time
fills in once a successful call (or a captured error response) populates
the API headers. It is a plain printed line (no terminal-positioning escape
sequences) and is not coupled to streaming or mouse mode, so it renders the
same on MobaXterm/Cygwin as on a native Linux terminal.
TAB completion (v0.6.6/v0.6.7/v0.7.0): TAB completion (v0.6.6/v0.6.7/v0.7.0):
Type '/' followed by any prefix and press TAB. Type '/' followed by any prefix and press TAB.
@ -5218,19 +5250,26 @@ main_loop() {
/cost) print_cost_summary; continue ;; /cost) print_cost_summary; continue ;;
/status) # v0.6.9: force-render the persistent status line on demand, /status) # v0.6.9: force-render the persistent status line on demand,
# e.g. when it has scrolled off-screen mid-conversation. # e.g. when it has scrolled off-screen mid-conversation.
# v0.8.7: ALWAYS render on explicit /status, even before the
# first turn. The turn-0 suppression in render_status_line is
# only for the AUTOMATIC between-turn render (nothing to report
# yet); an explicit /status is a deliberate request, so honor
# it and show the context/placeholder segments. This also lets
# Bryan verify the line renders on MobaXterm without first
# completing a (possibly rate-limited) turn.
if [ "${LARRY_NO_STATUS:-0}" = "1" ]; then if [ "${LARRY_NO_STATUS:-0}" = "1" ]; then
larry_say "status line disabled (LARRY_NO_STATUS=1)" larry_say "status line disabled (LARRY_NO_STATUS=1)"
else else
# Temporarily override the "first turn suppression" by # Lazy-init the context window so the ctx segment shows the
# making sure ctx_used has a value even if unknown. # right denominator even with zero turns / no API call yet.
[ -z "$STATUS_ctx_window" ] && STATUS_ctx_window=$(_model_context_window "$LARRY_MODEL") [ -z "$STATUS_ctx_window" ] && STATUS_ctx_window=$(_model_context_window "$LARRY_MODEL")
if [ -z "$STATUS_ctx_used_tokens" ] \ # Render directly via the auth-mode helper to bypass the
&& [ -z "$STATUS_oauth_5h_utilization" ] \ # turn-0 gate (which only applies to the automatic call).
&& [ "$_LARRY_TURNS" -eq 0 ]; then case "$LARRY_AUTH_MODE" in
larry_say "no data yet — make a turn first" oauth) _render_status_line_oauth ;;
else apikey) _render_status_line_apikey ;;
render_status_line *) _render_status_line_apikey ;;
fi esac
fi fi
continue ;; continue ;;
# v0.7.0: HL7 schema lookup commands. # v0.7.0: HL7 schema lookup commands.