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:
parent
578cefcc35
commit
4a992d9668
53
CHANGELOG.md
53
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
|
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).
|
||||||
|
|||||||
119
larry.sh
119
larry.sh
@ -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.9–v0.7.0). It now reads
|
# 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
|
# 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.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() {
|
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.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user