diff --git a/CHANGELOG.md b/CHANGELOG.md index 41d0770..ee40dae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 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 (`[ -le ]`) 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 Work-box → Mac `headers.log` sync (tsk-2026-05-27-023, Clover headers-sync). diff --git a/VERSION b/VERSION index 7fc2521..1e9b46b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.8.6 +0.8.7 diff --git a/larry.sh b/larry.sh index 6ad2d70..baa7e76 100755 --- a/larry.sh +++ b/larry.sh @@ -65,7 +65,7 @@ set -o pipefail # ───────────────────────────────────────────────────────────────────────────── # Config # ───────────────────────────────────────────────────────────────────────────── -LARRY_VERSION="0.8.6" +LARRY_VERSION="0.8.7" LARRY_HOME="${LARRY_HOME:-$HOME/.larry}" # ───────────────────────────────────────────────────────────────────────────── @@ -2626,32 +2626,47 @@ _parse_response_headers() { # agent_turn begins (was: above the prompt, v0.6.9–v0.7.0). It now reads # as a "between turns" marker summarising the just-completed turn's cost # 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. +# +# 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.9–v0.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() { [ "${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 - oauth) - # Suppress if we have NO context data AND no OAuth data — first turn. - 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 - ;; + oauth) _render_status_line_oauth ;; + apikey) _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 } @@ -2714,11 +2729,16 @@ _render_status_line_oauth() { else five_pct="—" fi - if [ -n "$STATUS_oauth_5h_reset_epoch" ]; then - if [ "$STATUS_oauth_5h_reset_epoch" -le "$now" ]; then + # v0.8.7: coerce_int the reset epoch before the `-le` test. These values come + # 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" else - five_reset="reset $(_epoch_to_hhmm "$STATUS_oauth_5h_reset_epoch")" + five_reset="reset $(_epoch_to_hhmm "$_5h_epoch")" fi else five_reset="reset —" @@ -2735,19 +2755,25 @@ _render_status_line_oauth() { else seven_pct="—" fi - if [ -n "$STATUS_oauth_7d_reset_epoch" ]; then - if [ "$STATUS_oauth_7d_reset_epoch" -le "$now" ]; then + # v0.8.7: coerce_int the 7d reset epoch — same CR-taint / arithmetic-crash + # 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" 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 else seven_reset="reset —" fi # 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="" - case "$STATUS_oauth_status" in + case "$(strip_cr "$STATUS_oauth_status")" in rate_limited) overall_pre="$C_RED" ;; warning) overall_pre="$C_YELLOW" ;; esac @@ -4318,13 +4344,19 @@ Multi-line input: 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). -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 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 ─ API key: ─ ctx 12% (24K/200K) ─ $0.213 session ─ 14 turns ─ - Disable entirely with LARRY_NO_STATUS=1. Force re-display with /status. - Suppressed automatically on the first turn (no data yet). + Disable entirely with LARRY_NO_STATUS=1. Force re-display anytime with + /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): Type '/' followed by any prefix and press TAB. @@ -5218,19 +5250,26 @@ main_loop() { /cost) print_cost_summary; continue ;; /status) # v0.6.9: force-render the persistent status line on demand, # 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 larry_say "status line disabled (LARRY_NO_STATUS=1)" else - # Temporarily override the "first turn suppression" by - # making sure ctx_used has a value even if unknown. + # Lazy-init the context window so the ctx segment shows the + # right denominator even with zero turns / no API call yet. [ -z "$STATUS_ctx_window" ] && STATUS_ctx_window=$(_model_context_window "$LARRY_MODEL") - if [ -z "$STATUS_ctx_used_tokens" ] \ - && [ -z "$STATUS_oauth_5h_utilization" ] \ - && [ "$_LARRY_TURNS" -eq 0 ]; then - larry_say "no data yet — make a turn first" - else - render_status_line - fi + # Render directly via the auth-mode helper to bypass the + # turn-0 gate (which only applies to the automatic call). + case "$LARRY_AUTH_MODE" in + oauth) _render_status_line_oauth ;; + apikey) _render_status_line_apikey ;; + *) _render_status_line_apikey ;; + esac fi continue ;; # v0.7.0: HL7 schema lookup commands.