Release notes

FEATURE

Logs now rotate, and tests stop polluting the real record

When a computer stays on for months, a log file can become a notebook that never turns a page: it only grows. launchd, macOS's background service launcher, had been redirecting output to ~/Library/Logs/usage/usage.log with no size limit. Usage now manages its own nearby log: each file is capped at 2 MB and three old copies are retained; even if it cannot create the directory, the app still starts and keeps console output. macOS uses ~/Library/Logs/usage/; other platforms use ~/.usage/logs/. Tests now write logs to a temporary directory too, so their warnings no longer mingle with the real usage-app.log used for diagnosis.

FEATURE

Debugging can now target selected parts instead of flooding everything

Trying to find out why pricing loaded oddly used to mean drowning in output from more than forty debug sites. USAGE_DEBUG, the switch that asks the program to print diagnostic messages, is no longer only all on or all off. Set the environment variable before launch to USAGE_DEBUG=codex_loader,pricing and only those two subsystems speak up; 1, all, and * still enable everything. Setting a.b also covers a.b.c, because Python's logger hierarchy already passes settings from a parent name to its children. It is like a concert board that can light only two performers instead of every lamp on the stage.

FEATURE

Health checks can now be compared by programs, not just screenshots

When someone asks for Usage's health-check result, usage --doctor --json now returns data a program can read rather than a block of text that needs a screenshot. Each check has a stable code, such as hook_state or codex_sessions, and one of three states. If any check is an error, the command ends with a non-zero exit code, so other tools can compare two reports or determine which check failed automatically. When the boundary is unclear, it chooses warn instead of falsely calling it an error. It is like a medical form with fixed columns: people and computers can both line up the results.

FEATURE

Menu items are assembled in one place while the bound method stays put

When maintaining a macOS menu bar, the trouble is often not adding a new option but repeating the same assembly steps for a dozen of them. switchPanel_ now uses menubar_menu.build_menu_item(), a helper function that gathers repeated steps in one place, to create the items; target, state, tooltip, and adding the item are written once. switchPanel_ itself stays where it is because AppDelegate binds it through a selector, the mechanism that connects a feature by method name. Move the original method and that connection breaks. This removes 24 repeated lines, not the 80-line illusion suggested by the count. The old test now also replaces NSMenuItem in the helper module instead of adding module-level indirection. It is like gathering a dozen sticky notes into one notebook: the items stay put, but copying stops repeating.

Original changelog

Original changelogLogs now rotate, and tests stop polluting the real record
**The file log is bounded and rotates.** The only log on disk was whatever launchd redirected into `~/Library/Logs/usage/usage.log`, and a redirect has no size limit — for a menu bar app that stays open for months, that file only ever grows. Python now owns a log of its own beside it, capped at 2 MB with three archives kept. Building the handler is best-effort: if the directory cannot be created the app keeps its console output and starts normally, because a logging subsystem must never be the thing that stops the program from running. macOS keeps the `~/Library/Logs/usage/` convention; every other platform writes under `~/.usage/logs/` next to the disk caches, since `~/Library/Logs` means nothing on Windows and `_setup_logging()` runs before the platform branch.
**The test suite wrote into the developer's real log file.** `tests/test_main.py` exercises `main()`, whose first action is `_setup_logging()` — so the run attached a rotating file handler pointed at the actual `~/Library/Logs/usage/usage-app.log`, and every warning logged by every later test landed in it. One full run deposited 8 KB containing pytest's temporary directory paths, which is worse than noise: it makes the log untrustworthy the day someone reads it for a real diagnosis. An autouse fixture now redirects the log directory to `tmp_path`, matching the existing rule for `cache_quarantine.QUARANTINE_DIR`. Curiously the same class of bug was fixed one version earlier in the Windows tray tests.
Original changelogDebugging can now target selected parts instead of flooding everything
**`USAGE_DEBUG` selects subsystems instead of switching everything on.** It was a boolean, so diagnosing one loader meant turning on all forty-odd debug sites and reading the result through the noise. It now takes a comma-separated list — `USAGE_DEBUG=codex_loader,pricing` raises only those two. Prefix matching comes free from Python's logger hierarchy rather than a hand-written comparison: setting `a.b` covers `a.b.c` because that is how logger inheritance already works. `1`, `all` and `*` keep the previous global behaviour.
Original changelogHealth checks can now be compared by programs, not just screenshots
**`--doctor --json` emits the report as data with stable codes.** The report was a block of text, so a user could only screenshot it and the reader had to scan by eye — nothing could compare two reports or decide programmatically which check failed. Each check now carries a fixed code (`hook_state`, `codex_sessions`, …) and one of three states, the process exits non-zero when any check is in error, and the human-readable output is unchanged to the character. Ambiguous cases resolve to `warn` rather than `error`: a false alarm that fails a health check costs more than a missed warning.
Original changelogMenu items are assembled in one place while the bound method stays put
**`switchPanel_` builds its items through a helper.** A dozen menu entries each repeated the same five to seven lines — allocate the item, set the target, set the state, set the tooltip, add it — and `menubar.py` had two lines left under its ceiling. `menubar_menu.build_menu_item()` absorbs that shape. It lives in a new module rather than in `menubar_state.py`, which stays free of PyObjC. `switchPanel_` itself does not move: an AppDelegate method is bound by selector, and relocating one breaks the binding. The saving is smaller than it looks — 24 lines, not the 80 the line count suggested, because keyword arguments wrap across lines and give much of it back. The gain is the removed repetition, not the arithmetic. Moving the helper out also broke a test that patched `NSMenuItem` on the `menubar` module only; the fix was to patch the helper module too, never to add module-level indirection so the old patch keeps reaching.