---
name: windows-spend-tray-counter
description: Build and maintain the Hermes spend counter that lives in Connie's Windows system tray (pystray). Trigger when the user asks about the tray $ icon, wants it rebuilt/moved to another machine (Surface), changed thresholds, or it disappears. Encodes the live data feed (gateway /api/analytics/usage), the working script location, and the pystray-win32 + Scheduled Task pitfalls hit during the 2026-08-09 build.
---

# Windows Spend Tray Counter (Hermes $ in system tray)

**⚠️ RETIRED 2026-08-11 — Rob killed the whole thing.** After the watchdog fiasco (5-min powershell task flashing console windows) and repeated detach/launch failures, Rob's verdict: "sick of spending time on tasks that don't get us any closer to earning a dollar." He keeps the **Nous dashboard open in a browser tab** for spend visibility instead. Tray process killed, `HermesSpendTray` at-logon task unregistered. The script still lives in the vault (`hermes_spend_tray.py`) if he ever wants it back, but **do not suggest rebuilding this unprompted.** The lessons below are kept as reference for the windows-tray-status-widget pattern and for what NOT to do (5-min scheduled-task watchdogs, `&`-launching tray apps).

---

A tiny pystray app on Connie shows live Hermes spend in the system tray — modeled on the Claude "session/weekly usage" tray icon. Icon draws **today's $ spend** on a colored rounded square; hover tooltip shows today + week; right-click menu shows by-model + Refresh / Open dashboard / Quit.

## Architecture (what already exists)

- **Data feed:** the gateway's own `GET /api/analytics/usage?days=N` (built into `hermes serve`, `web_server.py` line ~16057). Verified reachable from Connie via `https://robblake.cloud/api/analytics/usage` with the dashboard session token as `Authorization: Bearer <token>`. Returns `daily[]`, `by_model[]`, `totals.total_estimated_cost`.
- **Script (SSOT on VPS):** `/root/.hermes/vault/hermes_spend_tray.py` → Syncthing (`hermes-vault` folder) → Connie at `C:\Users\Rob\Documents\Obsidian Vault\hermes_spend_tray.py`. **Edit on the VPS, it syncs to Connie.** After any VPS-side edit, `chmod 644` the file (agent writes land `600` and can stall Windows-side visibility).
- **Token:** plaintext file `C:\Users\Rob\.hermes_spend_tray_token` (read by the script, never hard-coded). Token is the dashboard session token, pinned via `HERMES_DASHBOARD_SESSION_TOKEN` in `/etc/systemd/system/hermes-serve.service`, so it survives restarts. Rob decided 2026-08-09 to LEAVE the token as-is (it transited chat once; low-risk read-only scope).
- **Auto-start (SIMPLIFIED 2026-08-11 — watchdog RETIRED by Rob's call):** Scheduled Task `HermesSpendTray` runs `pythonw.exe "<vault>\hermes_spend_tray.py"` **at logon only**. No keep-alive. The old 5-min watchdog (`HermesSpendTrayWatchdog` + `hermes_spend_tray_watchdog.ps1`) was KILLED: running `powershell.exe` on a 5-min Task Scheduler trigger flashed a console window every tick (the "-WindowStyle Hidden" arg does NOT prevent Task Scheduler from briefly allocating a console host for powershell.exe). Rob's verdict: the guard caused more disruption than a dead icon — worst case he relaunches manually with the one-liner: `& "C:\Users\Rob\AppData\Local\hermes\hermes-agent\venv\Scripts\pythonw.exe" "C:\Users\Rob\Documents\Obsidian Vault\hermes_spend_tray.py"`. **Never reintroduce a 5-min scheduled-task watchdog for a tray app.** Also killed same day: `Daily Scout Exec Summary` and `RobBlake_ACPower_NoSleep` tasks (both unwanted powershell.exe timers).
- **Interpreter:** the Hermes venv pythonw on Connie — `C:\Users\Rob\AppData\Local\hermes\hermes-agent\venv\Scripts\pythonw.exe` (windowless). This is the python on PATH that has pystray/pillow/requests installed. Do NOT use the uv-managed `cpython-3.14` pythonw — deps are not there.
- **Crash log:** `C:\Users\Rob\.hermes_spend_tray.log` — fatal tracebacks land here. Watchdog log: `~\.hermes_spend_tray_watchdog.log`.

## CRITICAL pitfall — the tray process must be DETACHED, not window-owned

Hit 2026-08-09, **hit AGAIN 2026-08-11**: icon kept "vanishing." Root cause was NOT a crash — the `pythonw` process was a **child of an open terminal window**. Closing that window (even a blank one) killed the icon. **Never launch with `& pythonw.exe ...` from an interactive shell — `&` keeps the process window-owned.** Manually running `python hermes_spend_tray.py` OR `Start-ScheduledTask` from an interactive shell both produce a window-owned process too.

**The ONLY correct manual relaunch** (orphans the process so it survives window closes):

```powershell
Start-Process "C:\Users\Rob\AppData\Local\hermes\hermes-agent\venv\Scripts\pythonw.exe" -ArgumentList '"C:\Users\Rob\Documents\Obsidian Vault\hermes_spend_tray.py"' -WindowStyle Hidden
```

**Verify detachment, don't assume it:** get the tray `pythonw`'s `ParentProcessId`, then look up that parent — if the parent has ALREADY EXITED, the process is orphaned/detached (GOOD, survives window closes). If the parent is `powershell.exe`/`cmd.exe`/`WindowsTerminal.exe`, it's window-owned and will die when that window closes. Verification block: `Get-CimInstance Win32_Process -Filter "Name='pythonw.exe'" | ? {$_.CommandLine -like "*hermes_spend_tray.py*"} | % { $_.ProcessId, $_.ParentProcessId }` then resolve the parent PID. The decisive user-facing test: icon is up, then close ALL terminal windows — it must stay.

## Numbers it shows

- **Icon:** today's `estimated_cost` (from the `daily` row matching today's ISO date), `$X.X` under $10 else `$X`. Color: green < $5, amber < $20, red ≥ $20 (thresholds `GREEN_BELOW`/`AMBER_BELOW` at top of script — edit to taste).
- **Tooltip:** `Today $X.XX | Week $XX.XX` (week = Monday-start, matching Claude's weekly model).
- **Menu:** by-model breakdown for the week (greyed-out line), Refresh now, Open dashboard, Quit.
- Polls every 30s (`POLL_SECONDS`).

## pystray-win32 pitfalls (ALL hit 2026-08-09 — do not reintroduce)

1. **Tooltip is HARD-capped at 128 chars** (`Shell_NotifyIcon` `NOTIFYICONDATAW.szTip`). First build crashed `ValueError: string too long (131, maximum length 128)` because the by-model breakdown was in the tooltip. Keep tooltip short; put detail in the MENU (no length limit there). Always truncate `self.icon.title = tip[:127]`.
2. **Never call `icon.update_menu()` from the background poll thread.** pystray-win32 can throw when the menu is poked off the UI thread; an unhandled raise on that thread silently kills the icon (Windows sweeps it). Menu text is a dynamic callable (`Item(self._detail_text, ...)`) that refreshes on open — no explicit update needed.
3. **The poll loop must be unkillable.** Wrap `refresh()` AND `_repaint()` in try/except so no exception propagates and kills the thread. Symptom of an unhandled thread death: "icon was there for a minute, now gone."
4. **"Icon disappeared" is often NOT a crash** — Windows hides overflow icons when they update. First check: is it under the `^` overflow arrow? User drags it to the main tray to pin it. Don't rebuild before checking this.

## Hidden Scheduled Task pattern (pythonw, no window)

Needs an **admin** PowerShell (`Register-ScheduledTask` is Access-denied otherwise). Key: use `pythonw.exe` (windowless) not `python.exe`, and `-Hidden` + `-ExecutionTimeLimit ([TimeSpan]::Zero)` (no timeout kill). Full working block is in session history; the action is:

```powershell
$pyw="C:\Users\Rob\AppData\Local\hermes\hermes-agent\venv\Scripts\pythonw.exe"
$script="C:\Users\Rob\Documents\Obsidian Vault\hermes_spend_tray.py"
New-ScheduledTaskAction -Execute $pyw -Argument "`"$script`""
New-ScheduledTaskTrigger -AtLogOn -User $env:USERNAME
New-ScheduledTaskSettingsSet -Hidden -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -ExecutionTimeLimit ([TimeSpan]::Zero)
Register-ScheduledTask -TaskName "HermesSpendTray" ... -Force
```

Verify without reboot: `Start-ScheduledTask -TaskName "HermesSpendTray"; Start-Sleep 5; Get-Process pythonw`. (Confirm the registration block printed `Ready` first.)

## Two-step interpreter discovery (avoid wrong-path silent failure)

Before writing any Scheduled Task that runs a Python script on Connie, find the REAL interpreter — don't guess. `Get-Command pythonw` shows the on-PATH one; but confirm which python actually has the deps (the one the script ran under interactively). On Connie there are ≥3 pythons (Hermes venv, uv 3.11, uv 3.14). Guessing wrong = task fails silently at login, icon never appears, no error. One `Get-Command pythonw` + `Get-ChildItem ... -Filter pythonw.exe -Recurse` discovery block first, then bake the confirmed path into the task.

## Rebuild / move to Surface

1. Ensure `hermes-vault` Syncthing share reaches the target (Surface already pairs, path `C:\Users\rkbla\Documents\Obsidian Vault`).
2. `pip install pystray pillow requests` on the target.
3. Create `~\.hermes_spend_tray_token` with the token.
4. Run once interactively to confirm icon + numbers, then the Scheduled Task block with the target's confirmed pythonw path.

## The spend-side context (why this exists)

The counter is the early-warning; the habit is the fix. The 2026-08-09 $70/3-day spend was one 955-message desktop session re-sending its full history (89M cache-read tokens) across 418 API calls = $29. Cache_read_tokens (not input_tokens) is the leading spend signal on Moonshot/kimi. When the tray number climbs fast in a long session, that's the cue to `/new` + re-anchor with a handoff summary instead of continuing. See `operating-hermes-gateway` "Quantifying time/cost" for the state.db query (`session_model_usage` / `sessions.estimated_cost_usd`).
