Replace the footer's placeholder usage with a real observation ledger
The footer showed invented percentages. It now shows what two harnesses actually report, or says it does not know. lumbridge-core gains an append-only per-profile UsageLedger and a projection that labels every derived value estimated, withholds a burn rate from a single sample, withholds a window fraction with no reported ceiling, withholds an exhaustion estimate that lands after the reset, and reports an expired window as rolled over rather than freezing its last percentage. A missing fact renders as missing, never as zero. (0012) lumbridge-harness is the impure side: processes, clocks, and untrusted wire text in, observations out. Three adapters: - Codex's account/rateLimits/read over the app-server's JSON-RPC stdio. The client cannot express a request outside a two-variant enum and answers every server-to-client request with -32601, so a harness asking Lumbridge for a credential is refused by construction. (0013) - Claude Code's session transcripts, as a byte-offset tail follower that reports nothing until the backlog is read to EOF — a partially-read backlog is indistinguishable from a burst of spend, and the first run against 20 MB reported forty-six billion tokens an hour. The parser models four counters, so the conversations in those files are not representable. (0014) - Claude Code's five-hour and seven-day subscription windows, via a bridge installed as its statusLine command. 0014 had claimed no such surface existed; it does, and the record is corrected in place rather than quietly edited. Lumbridge does not read the OAuth credential to call the account usage endpoint, which is what comparable tools do — AGENTS.md forbids it, and 0015 says so rather than leaving the gap unexplained. Also in here: a capability-check ordering fix in the workspace reducer, where the applied-request replay table was consulted before the capability check and so answered questions the caller had no right to ask; the GPUI spike wired to the live probes with per-harness gauges and provenance chips; and a launcher that matches its own window by PID, because GPUI sets WM_NAME but not _NET_WM_NAME and a title match never succeeded. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
7fe84f71e2
commit
ef52aa7ce2
@@ -0,0 +1,108 @@
|
||||
"""Extract Claude Code's rate-limit windows from a status-line payload.
|
||||
|
||||
Invoked by `claude-statusline-bridge.sh` with the feed path as argv[1] and the
|
||||
status-line JSON on stdin. Appends only the rate-limit fields to the feed and
|
||||
prints a status line.
|
||||
|
||||
Must never fail: a status-line command that errors degrades the user's Claude
|
||||
Code session, so every step is guarded and this always exits 0 having printed.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Keep the feed bounded. The probe only needs the newest line; older ones are
|
||||
# superseded snapshots, not history worth keeping.
|
||||
MAX_FEED_BYTES = 256 * 1024
|
||||
WINDOW_NAMES = ("five_hour", "seven_day")
|
||||
PERCENT_KEYS = ("used_percentage", "utilization", "resets_at")
|
||||
|
||||
|
||||
def extract(payload):
|
||||
"""Return the record to append: rate-limit fields and nothing else.
|
||||
|
||||
The payload also carries the session's cost, transcript path, working
|
||||
directory, and model. None of that is Lumbridge's business, and none of it
|
||||
is copied — this builds a fresh dict rather than filtering the original.
|
||||
"""
|
||||
record = {"rate_limits_available": payload.get("rate_limits_available")}
|
||||
limits = payload.get("rate_limits")
|
||||
if not isinstance(limits, dict):
|
||||
return record
|
||||
windows = {}
|
||||
for name in WINDOW_NAMES:
|
||||
window = limits.get(name)
|
||||
if not isinstance(window, dict):
|
||||
continue
|
||||
kept = {
|
||||
key: window[key]
|
||||
for key in PERCENT_KEYS
|
||||
if isinstance(window.get(key), (int, float))
|
||||
and not isinstance(window.get(key), bool)
|
||||
}
|
||||
if kept:
|
||||
windows[name] = kept
|
||||
if windows:
|
||||
record["rate_limits"] = windows
|
||||
return record
|
||||
|
||||
|
||||
def status_line(record):
|
||||
if record.get("rate_limits_available") is False:
|
||||
return "lumbridge · no plan limits on this account"
|
||||
windows = record.get("rate_limits")
|
||||
if not windows:
|
||||
return "lumbridge · no window reported yet"
|
||||
parts = []
|
||||
for name, label in (("five_hour", "5h"), ("seven_day", "7d")):
|
||||
window = windows.get(name)
|
||||
if not window:
|
||||
continue
|
||||
percent = window.get("used_percentage", window.get("utilization"))
|
||||
if percent is None:
|
||||
continue
|
||||
parts.append(f"{label} {max(0.0, 100.0 - float(percent)):.0f}% left")
|
||||
if not parts:
|
||||
return "lumbridge · no window reported yet"
|
||||
return "lumbridge · " + " · ".join(parts)
|
||||
|
||||
|
||||
def append(feed, record):
|
||||
if not feed:
|
||||
return
|
||||
try:
|
||||
# Rotate rather than grow without bound. The probe resets its read
|
||||
# offset when the file shrinks, so a rotation costs one reading.
|
||||
if os.path.exists(feed) and os.path.getsize(feed) > MAX_FEED_BYTES:
|
||||
open(feed, "w").close()
|
||||
# Append so concurrent Claude Code sessions do not clobber each other;
|
||||
# a short line written in append mode lands intact.
|
||||
with open(feed, "a") as handle:
|
||||
handle.write(json.dumps(record, separators=(",", ":")) + "\n")
|
||||
except Exception:
|
||||
pass # A feed we cannot write is not worth breaking a session over.
|
||||
|
||||
|
||||
def main():
|
||||
feed = sys.argv[1] if len(sys.argv) > 1 else ""
|
||||
try:
|
||||
payload = json.load(sys.stdin)
|
||||
except Exception:
|
||||
print("lumbridge · unreadable status payload")
|
||||
return 0
|
||||
if not isinstance(payload, dict):
|
||||
print("lumbridge · unreadable status payload")
|
||||
return 0
|
||||
record = extract(payload)
|
||||
append(feed, record)
|
||||
print(status_line(record))
|
||||
return 0
|
||||
|
||||
|
||||
try:
|
||||
sys.exit(main())
|
||||
except Exception:
|
||||
# Last resort: never let a status line take a session down.
|
||||
print("lumbridge")
|
||||
sys.exit(0)
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/bin/sh
|
||||
# Lumbridge status-line bridge for Claude Code.
|
||||
#
|
||||
# Claude Code 2.1.80+ pipes a JSON payload to the configured `statusLine`
|
||||
# command on every turn. That payload carries `rate_limits.five_hour` and
|
||||
# `rate_limits.seven_day` — the real subscription windows, relayed from
|
||||
# rate-limit headers the CLI already received. Reading them here costs nothing
|
||||
# and needs no credential, which is why Lumbridge takes this route rather than
|
||||
# calling the account usage endpoint.
|
||||
#
|
||||
# This wrapper exists so a missing python3 degrades to a printed message
|
||||
# instead of a broken status line. It `exec`s the parser so stdin stays the
|
||||
# payload.
|
||||
#
|
||||
# Install with scripts/install-claude-statusline.sh; uninstall by removing the
|
||||
# statusLine block it adds to settings.json.
|
||||
|
||||
set -u
|
||||
|
||||
FEED="${LUMBRIDGE_CLAUDE_FEED:-${XDG_DATA_HOME:-$HOME/.local/share}/lumbridge/claude-rate-limits.jsonl}"
|
||||
HERE=$(dirname "$0")
|
||||
|
||||
if ! command -v python3 >/dev/null 2>&1; then
|
||||
# No parser available. Say so rather than printing a number we cannot read.
|
||||
echo "lumbridge · python3 not found"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$FEED")" 2>/dev/null || true
|
||||
|
||||
exec python3 "$HERE/claude-statusline-bridge.py" "$FEED"
|
||||
Executable
+113
@@ -0,0 +1,113 @@
|
||||
#!/bin/sh
|
||||
# Installs the Lumbridge status-line bridge into Claude Code's settings.
|
||||
#
|
||||
# This is the one thing Lumbridge cannot do without touching the user's
|
||||
# configuration, so it is a separate, explicit, reversible step rather than
|
||||
# something a probe does on your behalf.
|
||||
#
|
||||
# It sets `statusLine` in $CLAUDE_CONFIG_DIR/settings.json (default
|
||||
# ~/.claude/settings.json) to run scripts/claude-statusline-bridge.sh. Claude
|
||||
# Code then pipes its rate-limit payload there on every turn, which is how
|
||||
# Lumbridge learns the five-hour and seven-day subscription windows without
|
||||
# ever reading a credential.
|
||||
#
|
||||
# install: scripts/install-claude-statusline.sh
|
||||
# uninstall: scripts/install-claude-statusline.sh --uninstall
|
||||
# preview: scripts/install-claude-statusline.sh --dry-run
|
||||
#
|
||||
# An existing statusLine is backed up and reported, never silently replaced.
|
||||
|
||||
set -eu
|
||||
|
||||
MODE=install
|
||||
case "${1:-}" in
|
||||
--uninstall) MODE=uninstall ;;
|
||||
--dry-run) MODE=dry-run ;;
|
||||
"") ;;
|
||||
*) echo "usage: $0 [--uninstall|--dry-run]" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
CONFIG_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
|
||||
SETTINGS="$CONFIG_DIR/settings.json"
|
||||
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||
BRIDGE="$HERE/claude-statusline-bridge.sh"
|
||||
|
||||
if [ ! -x "$BRIDGE" ]; then
|
||||
echo "bridge not executable: $BRIDGE" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v python3 >/dev/null 2>&1; then
|
||||
echo "python3 is required to edit settings.json safely" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
python3 - "$SETTINGS" "$BRIDGE" "$MODE" <<'PYTHON'
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
|
||||
settings_path, bridge, mode = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
desired = {"type": "command", "command": bridge}
|
||||
|
||||
settings = {}
|
||||
if os.path.exists(settings_path):
|
||||
try:
|
||||
with open(settings_path) as handle:
|
||||
settings = json.load(handle)
|
||||
except Exception as error:
|
||||
print(f"refusing to edit unreadable settings: {error}", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
if not isinstance(settings, dict):
|
||||
print("refusing to edit: settings.json is not an object", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
|
||||
current = settings.get("statusLine")
|
||||
|
||||
if mode == "uninstall":
|
||||
if isinstance(current, dict) and current.get("command") == bridge:
|
||||
settings.pop("statusLine", None)
|
||||
action = "removed the Lumbridge status line"
|
||||
else:
|
||||
print("no Lumbridge status line installed; nothing to do")
|
||||
raise SystemExit(0)
|
||||
elif isinstance(current, dict) and current.get("command") == bridge:
|
||||
print("Lumbridge status line already installed; nothing to do")
|
||||
raise SystemExit(0)
|
||||
else:
|
||||
if current is not None:
|
||||
# Someone else's status line is here. Report it and stop rather than
|
||||
# replacing a thing the user set up.
|
||||
print("a different statusLine is already configured:", file=sys.stderr)
|
||||
print(f" {json.dumps(current)}", file=sys.stderr)
|
||||
print(
|
||||
"remove it first, or merge the bridge into it by hand:\n"
|
||||
f" {bridge}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
raise SystemExit(1)
|
||||
settings["statusLine"] = desired
|
||||
action = "installed the Lumbridge status line"
|
||||
|
||||
if mode == "dry-run":
|
||||
print("would write:")
|
||||
print(json.dumps({"statusLine": settings.get("statusLine")}, indent=2))
|
||||
raise SystemExit(0)
|
||||
|
||||
os.makedirs(os.path.dirname(settings_path), exist_ok=True)
|
||||
if os.path.exists(settings_path):
|
||||
backup = f"{settings_path}.lumbridge-backup-{int(time.time())}"
|
||||
shutil.copy2(settings_path, backup)
|
||||
print(f"backed up existing settings to {backup}")
|
||||
|
||||
# Write via a temporary file so an interrupted run cannot leave settings.json
|
||||
# truncated — this file configures the user's editor session.
|
||||
temporary = f"{settings_path}.lumbridge-tmp"
|
||||
with open(temporary, "w") as handle:
|
||||
json.dump(settings, handle, indent=2)
|
||||
handle.write("\n")
|
||||
os.replace(temporary, settings_path)
|
||||
print(f"{action} in {settings_path}")
|
||||
print("restart or start a Claude Code session for it to take effect")
|
||||
PYTHON
|
||||
@@ -44,11 +44,31 @@ if [[ ! -x "$binary_path" ]]; then
|
||||
cargo build --locked --manifest-path "$manifest_path"
|
||||
fi
|
||||
|
||||
existing_id=$(wmctrl -l | awk -v title="$window_title" 'index($0, title) { print $1; exit }')
|
||||
# GPUI's X11 client sets WM_NAME but not _NET_WM_NAME, so wmctrl's window list
|
||||
# reports its title as N/A and a title match never succeeds. xdotool reads
|
||||
# WM_NAME, and matching the launched PID avoids picking up another client that
|
||||
# merely mentions the window title.
|
||||
window_for_pid() {
|
||||
local pid=$1 candidate
|
||||
for candidate in $(xdotool search --name 'GPUI workspace' 2>/dev/null); do
|
||||
if [[ "$(xdotool getwindowpid "$candidate" 2>/dev/null || true)" == "$pid" ]]; then
|
||||
printf '%s\n' "$candidate"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
existing_id=''
|
||||
app_pid=$(pgrep -f "^$binary_path$" | head -n 1 || true)
|
||||
if [[ -n "$app_pid" ]]; then
|
||||
existing_id=$(window_for_pid "$app_pid" || true)
|
||||
fi
|
||||
if [[ -z "$existing_id" ]]; then
|
||||
"$binary_path" >"$XDG_RUNTIME_DIR/lumbridge-gpui.log" 2>&1 &
|
||||
app_pid=$!
|
||||
for _attempt in $(seq 1 80); do
|
||||
existing_id=$(wmctrl -l | awk -v title="$window_title" 'index($0, title) { print $1; exit }')
|
||||
existing_id=$(window_for_pid "$app_pid" || true)
|
||||
[[ -n "$existing_id" ]] && break
|
||||
sleep 0.1
|
||||
done
|
||||
@@ -59,23 +79,43 @@ if [[ -z "$existing_id" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Gigabyte G34WQC is 3440x1440 at X=0. Lumbridge owns its left half; Bacon
|
||||
# remains visible beside it on the right half of the same monitor.
|
||||
bacon_id=$(wmctrl -l | awk -v title="$bacon_title" 'index($0, title) { print $1; exit }')
|
||||
if [[ -n "$bacon_id" ]]; then
|
||||
wmctrl -i -r "$bacon_id" -b remove,maximized_vert,maximized_horz
|
||||
# GNOME's X11 move coordinate is scaled on this display even though window
|
||||
# sizes are not; 860 places the frame at physical X=1720.
|
||||
wmctrl -i -r "$bacon_id" -e 0,860,0,1720,1400
|
||||
# Gigabyte G34WQC is 3440x1440 at X=0. Lumbridge owns the whole panel: the
|
||||
# workspace is a wall of agents and every pixel of width buys another column.
|
||||
# Set LUMBRIDGE_HALF=1 to fall back to the left half beside Bacon.
|
||||
if [[ "${LUMBRIDGE_HALF:-0}" == "1" ]]; then
|
||||
bacon_id=$(wmctrl -l | awk -v title="$bacon_title" 'index($0, title) { print $1; exit }')
|
||||
if [[ -n "$bacon_id" ]]; then
|
||||
wmctrl -i -r "$bacon_id" -b remove,maximized_vert,maximized_horz
|
||||
# GNOME's X11 move coordinate is scaled on this display even though window
|
||||
# sizes are not; 860 places the frame at physical X=1720.
|
||||
wmctrl -i -r "$bacon_id" -e 0,860,0,1720,1400
|
||||
fi
|
||||
wmctrl -i -r "$existing_id" -b remove,fullscreen
|
||||
wmctrl -i -r "$existing_id" -b remove,maximized_vert,maximized_horz
|
||||
wmctrl -i -r "$existing_id" -e 0,0,0,1720,1400
|
||||
wmctrl -i -a "$existing_id"
|
||||
# GPUI's undecorated X11 client ignores direct position requests under GNOME.
|
||||
# Use the window manager's move interaction to settle it into the left half.
|
||||
xdotool key alt+F7
|
||||
sleep 0.15
|
||||
xdotool mousemove 860 660 click 1
|
||||
echo "Lumbridge is on the left half of the Gigabyte display (window $existing_id)."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Fullscreen drops the title bar and the shell owns all 3440x1440. Move it onto
|
||||
# the Gigabyte first: a fullscreen request applies to whichever output the
|
||||
# window is currently on, so placing it after would fullscreen the wrong panel.
|
||||
wmctrl -i -r "$existing_id" -b remove,fullscreen
|
||||
wmctrl -i -r "$existing_id" -b remove,maximized_vert,maximized_horz
|
||||
wmctrl -i -r "$existing_id" -e 0,0,0,1720,1400
|
||||
wmctrl -i -a "$existing_id"
|
||||
# GPUI's undecorated X11 client ignores direct position requests under GNOME.
|
||||
# Use the window manager's move interaction to settle it into the left half.
|
||||
xdotool key alt+F7
|
||||
sleep 0.15
|
||||
xdotool mousemove 860 660 click 1
|
||||
sleep 0.2
|
||||
wmctrl -i -r "$existing_id" -b add,fullscreen
|
||||
sleep 0.3
|
||||
|
||||
echo "Lumbridge is visible in window $existing_id on the left half of the Gigabyte display."
|
||||
geometry=$(xdotool getwindowgeometry "$existing_id" | awk '/Geometry/ { print $2 }')
|
||||
echo "Lumbridge is fullscreen on the Gigabyte display at ${geometry} (window $existing_id)."
|
||||
|
||||
Reference in New Issue
Block a user