Release notes

Changelog

Every shipped version of Halton Meter, newest first.

macOS 12+ · Python 3.11+ Reading time 2 min Updated May 11, 2026

v0.3.0 · 2026-05-29

The daemon now classifies every provider error locally into seven operator-action buckets, written to local SQLite and read by the bundled dashboard.

Added

  • Seven canonical error buckets — rate_limit, server_error, bad_request, auth, timeout, network, unknown. Each maps to a single operator action (back off, fix billing, wait it out, fix the request shape) regardless of provider. Anthropic, Gemini, OpenAI, and Grok/xAI all classify in this release. See /docs/concepts/error-classification.
  • Four nullable fields on every captured row — error_class (string), provider_error_code (string), http_status (smallint), retryable (bool). Written to ~/.halton-meter/db.sqlite and surfaced in the bundled dashboard alongside captured-cost rows. No enum on error_class; unknown buckets are tolerated, so future buckets need no schema migration.

Changed

  • Two judgement calls baked into the classifier: Anthropic HTTP 529 (overloaded_error) buckets as server_error not rate_limit (provider-overload, not a per-key throttle); OpenAI HTTP 429 insufficient_quota buckets as auth not rate_limit (exhausted billing, not retryable).
  • Backward compatible. Pre-v0.3.0 rows carry none of the four fields and remain valid; any error-rate view falls back to the legacy status != 'success' predicate. Connect to Halton Meter Cloud to get the aggregated, persona-aware error_observations surface across projects.

v0.2.11 · 2026-05-24

Windows apps-mode (phase 0, public beta). Per-user proxy via HKCU registry, `certutil` for cert trust, Task Scheduler for `ONLOGON` autostart — no admin required.

Added

  • Windows 10/11 apps-mode install (halton-meter init). Writes HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings\ProxyEnable=1, ProxyServer=127.0.0.1:8081, ProxyOverride="<local>", broadcasts WM_SETTINGCHANGE so running WinINet/Electron apps pick up the proxy without restart. HTTPS_PROXY / HTTP_PROXY / NO_PROXY written to HKCU\Environment so new shells inherit. Snapshot of prior registry state captured to ~/.halton-meter/system_state.json and restored on uninstall.
  • Per-user Task Scheduler tasks for halton-meter daemon and halton-meter edge (/SC ONLOGON). No admin required for HKCU tasks; the supervisor restarts on every interactive logon.
  • Per-user cert trust via certutil -user -addstore Root <cert_path> — installs the mitmproxy CA into the current user's Root store. Removed on halton-meter uninstall. No admin prompt.

Changed

  • Documentation: Windows install ships as **public beta**. WSL2 guest traffic and Windows machine-wide (HKLM) full-mode are deferred post-v1.0; WinHTTP-only clients (e.g. some .NET tools, Windows Update) need a one-shot netsh winhttp import proxy source=ie after init.

Fixed

  • POST /v1/cloud/connect cancels the previous pairing task before replacing _active so a repeated halton-meter cloud connect (or browser Connect button) cannot leave a zombie awaiting-approval task.

v0.2.10 · 2026-05-24

Attribution correctness + memory hygiene. Cached rows now carry `source_workdir`, large request/response bodies are skipped before they hit memory, and the attribution-store init race is closed.

Added

  • attribution_log.source_workdir TEXT column (fail-open ALTER TABLE). Every cached row from v0.2.10 forward carries the originating directory; historical rows show NULL. Cloud upload privacy still gates whether source_workdir is shipped — local capture is unconditional.
  • Body capture size gate. proxy.capture_body() returns early for any request or response body greater than 4 MiB and logs addon.body_capture.skip reason=body_too_large. Prevents memory spikes from large multi-modal payloads. Metadata for the request (model, tokens, cost) still captures; only the body is dropped.

Fixed

  • Attribution-store schema-init TOCTOU. Under high concurrency at process startup, the CREATE TABLE IF NOT EXISTS could race against the first SELECT, surfacing as sqlite3.OperationalError: no such table: attribution_log. The init path now holds an exclusive lock for the duration of schema creation.
  • Log event naming. cloud.worker.sync_paused event was emitted with reason="paused_unauthorised" even on the 403 path; now distinguishes paused_unauthorised (401) from paused_forbidden (403).

Removed

  • Dead code: psutil.net_connections() scan in tagging._get_process_name. The path was never reached in production and was a slow per-call socket walk on macOS. Removed without behaviour change.

v0.2.9 · 2026-05-24

Daemon hardening: loopback bind guard, dependency upper-bound caps, log rotation, and the report row cap is gone.

Added

  • Daemon log rotation. ~/.halton-meter/daemon.out.log and daemon.err.log rotate at 100 MiB with 5 backups (logging.handlers.RotatingFileHandler). Old rotations are kept on disk; nothing is sent off-machine.
  • Loopback bind guard (halton_meter/security.py). On startup, the daemon hard-exits with SystemExit(1) if listen_host or api_host resolves to a non-loopback address. Override with HALTON_METER_ALLOW_NON_LOOPBACK=1 for container or VPN topologies that legitimately bind non-loopback — the override logs a warning on every start.

Changed

  • Dependency upper bounds pinned in pyproject.toml: mitmproxy>=10.0,<13, pydantic>=2.0,<3, httpx>=0.27,<1, click>=8.1,<9. Stops a future major release from breaking pipx upgrade / uv tool upgrade until the daemon is tested against it.

Removed

  • halton-meter report row cap. storage.read_records limit parameter is now int | None = None (previously hard-coded to 10,000). Reports that scan the full local history no longer silently truncate.

v0.2.8 · 2026-05-24

Strict pause classification + a real `halton-meter cloud resume`. Transient errors no longer flip the daemon into a state that needs re-pairing.

Added

  • CloudForbidden exception class. HTTP 403 on /v1/requests/batch is now classified paused_reason="paused_forbidden". Recovery path: the workspace owner re-invites the machine, then halton-meter cloud resume clears the pause — re-pairing (cloud connect) would revoke the still-valid existing key.
  • cloud_sync_state.last_error_at column (user_version 6 → 7). Surfaced in halton-meter cloud status so operators can see how long ago the last transient error fired without grepping logs.

Changed

  • sync.paused_unauthorised triggers strictly on HTTP 401. All transient errors — 5xx, RemoteProtocolError, ReadTimeout, ConnectTimeout, httpx.ConnectError / DNS failure, HTTP 429, HTTP 422/400 — route through a new error_writer seam that never touches paused_reason. Network blips no longer present as "your token was revoked"; only a real 401 does.
  • halton-meter cloud resume is a real recovery command, not a no-op. It reads the current pause reason; on paused_manual it clears immediately. On paused_unauthorised / paused_forbidden it hits GET /v1/daemon/whoami once with the stored token — if 200, it clears the pause, counters, and last_error, unblocking the daemon without burning a cloud connect.

v0.2.7 · 2026-05-23

Hotfix: auto-heal placeholder `base_url` values left over from spike scripts.

Fixed

  • Auto-heal [cloud].base_url when it contains a placeholder TLD (.test, .example, .invalid, .local). The daemon rewrites the value to https://api.haltonmeter.com on next boot, logs the rewrite once, and proceeds. Catches stale config from staging / development scripts that would otherwise route every cloud request into a black hole.

v0.2.6 · 2026-05-23

Cloud onboarding loopback API. The browser-side onboarding shell at `app.haltonmeter.com` can now detect a running local daemon and pair it without copy/paste.

Added

  • GET /v1/cloud/state — returns {paired, version, hostname, port, ...} over loopback. Lets the dashboard's onboarding flow detect whether a local daemon is running and skip the install / start steps when it is.
  • POST /v1/cloud/connect — triggers a pairing-code mint without the user copy/pasting from a terminal. The endpoint returns {code}; the browser polls GET /v1/cloud/connect/status for approval, then completes pairing.
  • Chrome Private Network Access header Access-Control-Allow-Private-Network: true. The cross-origin browser fetch from https://app.haltonmeter.com to the daemon's loopback API now works without dev-mode flags on modern Chromium.

v0.2.5 · 2026-05-22

SaaS-launch release. Cursor cold-start attribution recovery, edge `unattributed` leak fix, friendlier transport-error UX, and the official backend URL is locked to `api.haltonmeter.com`.

Added

  • Cursor cold-start lenient-slug scan (attribution.layers.find_project_root_by_slug). Fires on Tier 4b / 4c registry miss; does a one-level scan of ~/Documents and ~ to warm the EdgeAttributionRegistry for the originating process. Recovery telemetry: 4b_lenient / 4c_lenient in attribution.tier_hit.
  • Backfill v2 — daemon/scripts/backfill_body_paths.py recovers Cursor-style rows by scanning file:// URIs and bare absolute paths in captured request bodies. Dry-run by default; --apply gated.

Changed

  • Friendly cloud.transport_error UX. The error message now surfaces the base URL and the config file path. Placeholder TLDs (.test, .example, .invalid, .local) raise immediately rather than burning the full retry budget; v0.2.7 layered auto-heal on top.
  • Official backend host locked. cloud/constants.py:HALTON_METER_CLOUD_URL = "https://api.haltonmeter.com" is now the production default. HALTON_METER_CLOUD_URL env var and --base-url flag remain as dev / staging overrides.
  • CI matrix adds Python 3.14 (experimental, continue-on-error: true) alongside 3.11 / 3.12 / 3.13.

Fixed

  • Edge unattributed/edge_store leak closed. edge_attribution._resolve_via_psutil Step 8 calls attribution.resolver.resolve_unified as a final fallback after Steps 1–7.5 exhaust. Recovery telemetry: edge_attribution.resolver_fallback (INFO) emitted whenever the fallback wins.

v0.2.4 · 2026-05-18

Unified, IDE-agnostic attribution resolver. Closes the edge-side `unattributed` / `edge_store` leak from v0.2.3 and widens the Python pin to `<3.15`.

Added

  • IDE-agnostic workspace recovery (Cursor, VSCode, Kiro, Zed, IntelliJ). Two new attribution layers — ide_env_label (reads CURSOR_WORKSPACE_LABEL / VSCODE_WORKSPACE_LABEL from the originating process's environment) and ide_argv_label (Electron-helper trailing-token regex against argv). Both corroborate against a per-edge slug → abspath registry so a stale or moved project cannot be impersonated.
  • Per-tier attribution.tier_hit telemetry. Structured event on every tier match AND every strict-slug miss (4b_miss, 4c_miss) — lets the operator grep ~/.halton-meter/daemon.err.log to confirm which resolver tier won per row.

Changed

  • Single attribution code path. The v0.2.3 daemon-side parent-PID walk and the edge-side walk are collapsed into one attribution.resolver.resolve_unified entry point shared by the daemon's bypass path and the edge's pin path. Step 7.25 and its four helpers in tagging.py are deleted; the chromium-helper gate and IDE-family allow-list in edge_attribution.py are deleted. Attribution policy lives in exactly one place.
  • Cross-uid / cross-session walk boundary. The parent walk no longer stops on a process-name allow-list — it terminates the moment a parent's uid or controlling session differs from the originating process. Same defensive intent, applies uniformly to every IDE, no list to maintain.
  • Python pin widened from <3.14 to <3.15. Plain pipx install halton-meter now succeeds on the system interpreter when macOS's default Python is 3.14. Hard dependencies (pydantic_core, mitmproxy, aiosqlite) ship 3.14 wheels.

v0.2.3 · 2026-05-18

Two correctness fixes. Long-lived CONNECT tunnels now pin attribution past the 5-minute TTL, and macOS plist writes are atomic against same-day reinstall sequences.

Fixed

  • Attribution: pin long-lived CONNECT tunnels. Claude Code holds tunnels open for hours during a single session; once 5 minutes elapsed the edge-store row became invisible to the daemon and every subsequent request fell through to smart_default. The read-side TTL filter on attribution_store.lookup is removed; the write-side prune loop is unchanged but now passes max_age_s=86400.0 (24h) so in-flight tunnels cannot have their rows evicted.
  • Attribution: daemon-side bounded parent-PID walk for the claude family. When the originating process's cwd resolves to None or / (the production failure mode for Claude Code's Mach-O tool-loop workers), the walk consults up to 5 ancestors via psutil and re-runs the rcfile / git / mac_sandbox / workdir-basename layers against the first ancestor's cwd. Cardinal rule preserved: any exception in the walk path is caught silently and the request falls through to the next attribution layer.
  • macOS plist atomic-write hardening. Every macOS LaunchAgent plist (daemon, watchdog, edge, userenv) now writes via a single _atomic_write_bytes helper — write to <name>.tmp, fsync, os.replace over the canonical path. Fixes the same-day-reinstall race that surfaced as an 8-byte <plist/> stub on the operator's edge LaunchAgent on 2026-05-18.

v0.2.2 · 2026-05-12

End-to-end body sync. Daemon now pushes captured request and response bodies to the cloud on the same opt-in posture as metadata — disabled by default, per-project overrides win over the master switch.

Added

  • [cloud.bodies] config block. enabled (bool, default false), sync_interval_seconds (default 60), max_body_bytes_per_upload (default 524288 — 512 KiB), per_project (dict of slug → bool). Bodies do not leave the machine until bodies.enabled = true.
  • halton-meter cloud privacy set bodies.enabled true|false — flips the master body-sync switch. halton-meter cloud privacy set bodies.upload false --project SLUG drops body uploads for one project while leaving the global switch on. halton-meter cloud privacy show renders a "Body sync" section with master state, interval, byte cap, and per-project rules.
  • BodyUploader worker. Sibling to the metadata worker; reads request_bodies WHERE uploaded_at IS NULL, POSTs two envelopes per row (request + response) to POST /v1/requests/{id}/body, stamps uploaded_at on success. Pause/last-error tracked independently in cloud_body_sync_state so a body 401 doesn't pause metadata sync.
  • Schema migration 5 → 6 adds request_bodies.uploaded_at DATETIME NULL plus a supporting index. Idempotent — safe on greenfield and upgrade-in-place installs.

v0.2.1 · 2026-05-12

Patch release. Operational bugs and CLI UX polish for the `cloud` subgroup.

Changed

  • CLI UX for the cloud subgroup matches halton-meter status. Rich panels and tables replace raw dicts; cloud status adds a state banner (ACTIVE / DEGRADED / PAUSED / NOT-CONFIGURED) plus per-field health icons; cloud reconcile adds a daemon-vs-cloud variance line (zero = green check, <0.5% = yellow tolerance, otherwise red). --json flag on cloud status still emits the machine-readable shape for scripting.

Fixed

  • cloud connect --base-url <URL> now persists the URL to ~/.halton-meter/config.toml. Pre-0.2.1, the flag was a transient override — pairing would succeed but every subsequent command would read [cloud].base_url from TOML, find it empty, and report the daemon as un-paired. Now the connect flow writes base_url and enabled=true next to the 0600 credentials write.
  • halton-meter cloud reconcile no longer 422s. Daemon sent from/to query params; cloud's reconciliation router expects from_date/to_date.

v0.2.0 · 2026-05-12

First minor release. Closes the Phase 2 cloud-sync arc (daemon ↔ halton-meter-cloud) and ships upload-privacy controls. Cloud sync is strictly opt-in — the daemon ships with `cloud.enabled = false`.

Added

  • halton-meter cloud CLI group. connect, disconnect, status, whoami, sync, reconcile, pause, resume. Pairing handshake (pairing/start → user approves in dashboard → pairing/poll) mints a single-shot hm_sync_… token. Token stored in ~/.halton-meter/cloud-credentials.json (chmod 0600) with a Fernet-encrypted mirror in SQLite.
  • Cloud worker supervised as part of the daemon process. Same-process spawn alongside the proxy / API / heartbeat tasks; gated on cloud_state.enabled so users who don't pair see zero change. Crash-isolated — a cloud-side failure (transport, 401, schema mismatch) cannot block the proxy hot path.
  • Upload privacy. Tiered consent presets (minimal / standard / full) under [cloud.upload] in ~/.halton-meter/config.toml. Default is standard with source_workdir = false — the high-leak field (local path) is off by default. Per-field global overrides; per-project rules (upload = false makes a project local-only). The daemon redacts before serialisation; the cloud cannot see what the daemon never sends.
  • halton-meter cloud privacy show|set. show prints the resolved policy; set preset standard / set field.source_workdir false / set upload false --project acme-secret mutate the TOML in place (other sections preserved).
  • Observability + lifecycle. Daemon supervisor logs cloud.supervisor.spawned / cloud.supervisor.skip with structured reason. cloud_state row tracks workspace name, machine id, hostname snapshot, last-connected-at, paused_reason. Worker retry envelope: exponential backoff capped at 300s for transport + 5xx + 429 (with Retry-After honoured). 401 never retried.

v0.1.24 · 2026-05-11

Linux daemon enters public beta. Apps-mode install on Ubuntu 24.04 LTS via `pipx` + `systemd --user`. macOS unchanged.

Added

  • Linux support (public beta). halton-meter init --apps brings up two systemd --user units — halton-meter.service (daemon + mitm + FastAPI) and halton-meter-edge.service (always-on edge proxy). Verified on Ubuntu 24.04 LTS via Docker soak and AWS Lightsail with real reboot, Claude Code, and Gemini captures. Other systemd distros likely work but are not yet covered by the release gate.
  • Per-OS install paths: macOS uses launchd + Keychain trust + shell-rc edits; Linux uses systemd --user + loginctl enable-linger + certifi-bundle patching (system trust store untouched).
  • Linux operator reference at docs/DAEMON_LINUX.md in the daemon repo — every command, path, port, unit name, and endpoint cross-checked against the source.

Changed

  • Version scheme collapses from four-segment (v0.1.22.x) to three-segment (v0.1.24) ahead of the v1 cut. Internal nightly cadence continues; the public PyPI tag is the headline.
  • Daemon systemd unit declares Restart=always RestartSec=1 and an ExecStop=halton-meter reset-proxy belt-and-braces so even a SIGKILL leaves the user's network state clean.
  • halton-meter doctor edge-proxy row on Linux uses socket.connect_ex for the port-bound check — works inside containers and non-systemd environments without raising. User-facing contract is "the port is bound", not "systemd thinks the unit is running".

v0.1.22.24 · 2026-05-08

LLM host allowlist gates which traffic the edge chains through the mitm interceptor. Bundled with the v0.1.22.23 freshness-probe URL fix.

Added

  • LLM host allowlist controls which hostnames the edge proxy chains through the mitm interceptor. Non-LLM traffic bypasses interception entirely; only allowlisted provider hosts get the captured-cost treatment.
  • mitmproxy ignore_hosts is now derived as the inverse of the LLM allowlist, so the interceptor itself only attaches to traffic the daemon cares about.
  • halton-meter doctor surfaces the active intercept allowlist. One row, mono, exactly the hostnames the daemon will capture.

Fixed

  • Freshness probe URL pointed at the private haltonlabs/halton-meter repo via raw.githubusercontent.com, which returned 404 to unauthenticated clients — halton-meter doctor was silently fail-open on every install. Probe now targets https://haltonmeter.com/rates-manifest.json, hosted out of the public landing repo. (v0.1.22.23 fix, bundled into this release.)

v0.1.22.22 · 2026-05-08

First public release. Local-binary daemon, six adapters across four providers, captured-cost terminal report, bundled dashboard.

Added

  • Daemon ships as a local binary on PyPI. pipx install halton-meter followed by halton-meter init brings up the three-process architecture: edge on 127.0.0.1:8081, mitm listener on 127.0.0.1:8090, loopback API on 127.0.0.1:8765.
  • Six adapters across four providers — OpenAI, Anthropic, Gemini, Grok. Direct API surfaces and OAuth surfaces (ChatGPT, Gemini Code Assist) both intercepted without SDK changes.
  • Captured-request store: full request and response bodies, redacted, written to ~/.halton-meter/db.sqlite. Schema documented; halton-meter report slices it from the terminal.
  • Project attribution chain: env-var override, working-directory match, process-tree walk. Configured per-project via ~/.halton-meter/projects.toml.
  • Bundled dashboard at localhost:3000. Open source under Apache 2.0 (separate halton-meter-dashboard repo). Read-only; reads the daemon's loopback API.
  • CLI surface: init, start, stop, status, report, uninstall. Fail-open behaviour — when the daemon stops, traffic passes through the edge unmodified.
  • Structured logs under ~/.halton-meter/*.err.log (one pair per process). jq-queryable; documented in /docs/operations/logs.

Changed

  • Init self-test retries every 500ms for up to 30 seconds (replaces the single-shot health check). Cold-start no longer reports false negatives.