Skip to content

fix(desktop): parse the usage-window layout so Nous rows survive - #56

Merged
Adolanium merged 1 commit into
Adolanium:mainfrom
jorisquintano:fix/usage-window-parse
Oct 30, 2026
Merged

Adolanium merged 1 commit into
Adolanium:mainfrom
jorisquintano:fix/usage-window-parse

Conversation

@jorisquintano

Copy link
Copy Markdown
Contributor

Symptom

The Nous Portal rows disappear from the Resetwatch page while every other provider still renders. No error banner is shown — the block silently parses to zero windows.

Cause (measured)

For an account metered by usage windows (no monthly credit grant), agent/billing_usage.py::build_usage_model() short-circuits:

if _window_seed_fields(getattr(account_info, "usage_window", None)) is not None:
    return UsageModel(available=False)

so the two RPC paths Resetwatch uses for Nous (usage.bars, then subscription.state) return available: false and produce no cards. The only path left is the slash.exec usage text, parsed by parseUsageOutput().

That text, for such an account, is (verbatim from a real gateway, only the numbers are this reporter's):

Hermes Plus
Today                   [|         ]  1% used  ·  resets in 23h
This week               [|         ]  <1% used  ·  resets in 6d 23h
This month              [|         ]  <1% used  ·  resets in 4d 5h
Large models (monthly)  [          ]  0% used  ·  resets in 4d 5h
Keep going on credits: off. Hermes waits for the reset (turn on in /topup).
Top-up credits: $24.27
Renews: Nov 3, 2026
Top up via /topup

parseUsageOutput() matches neither the plan-name header (Hermes Plus is not Account limits / Nous credits) nor the gauge lines (it expects Label: N% remaining (M% used)), so groups is empty and the row is silently dropped. Reproduced against main (5b6c9c6) and the 0.2.26 catalog package.

Fix

Two additions to parseUsageOutput(), nothing else changes:

  • a plan-name header (Nous Portal, Nous credits, Hermes Free|Plus|Pro|Max|Team|Business, Insider) opens a group with provider: 'Nous Portal';
  • gauge lines are parsed as windows: remaining = 100 - used, and <1% used is read as 0 used (it is a rounding floor, not a measurement).

The existing account-limits layout (Label: N% remaining (M% used), Provider: …) is untouched.

Tests

Two tests added to test_quick_usage.cjs (the harness now exports parseUsageOutput): the window layout yields 4 Nous windows with the right remaining/used/reset text and keeps the detail lines, and the account-limits layout still parses as before.

Measured locally on Linux, Node 22.23.1:

  • node --test test_quick_usage.cjs → 44/44 pass
  • node --test test_profile_routing.cjs test_probe_pagination.cjs test_quick_usage.cjs test_i18n.cjs test_shell_exec_session.cjs → 103/103 pass
  • HERMES_TEST_CATALOG=1 node --test test_profile_routing.cjs test_quick_usage.cjs test_i18n.cjs → 75/75 pass
  • python scripts/build_catalog.py --check → Catalog package verified
  • node --check plugin.js and node --check catalog/desktop/plugin.js → clean

catalog/desktop/plugin.js is the regenerated artifact (python scripts/build_catalog.py), as the release commits do.

Not verified

  • Providers other than Nous on a window plan (the parser change is provider-agnostic, but only Nous was measured here).
  • The Desktop app itself: the parsing is pure JS and was tested in the vm harness only; the reproduction above is from a Linux gateway, not from a Windows Desktop.

Used locally as a patch on 0.2.26 before opening this; happy to adjust the header list if you'd rather match the plan names strictly, and to drop the plan-name group in favour of an explicit "no windows parsed" error if you prefer surfacing the failure over guessing the provider.

Hermes renders an account metered by usage windows (no monthly credit
grant) as a plan-name header plus one gauge line per window:

    Hermes Plus
    Today                   [|         ]  1% used  ·  resets in 23h
    This week               [|         ]  <1% used  ·  resets in 6d 23h
    Top-up credits: $24.27
    Renews: Nov 3, 2026

parseUsageOutput only matched the account-limits layout
("Label: N% remaining (M% used)") and the fixed "Account limits" /
"Nous credits" headers, so a window block parsed to zero windows and the
Nous rows disappeared silently.

Those rows have no other source: usage.bars and subscription.state both
return available:false for a window plan (build_usage_model short-circuits
when _window_seed_fields() is not None), so the slash.exec usage text is
the only path that can carry them.

Accept the plan-name header as a group opener and parse the gauge lines,
reading "<1% used" as zero used. The account-limits layout is unchanged.
@Adolanium
Adolanium merged commit 47e6f47 into Adolanium:main Oct 30, 2026
1 check passed
@Adolanium

Copy link
Copy Markdown
Owner

Merged, thank you @jorisquintano, and welcome!

You traced the missing rows past the plugin to build_usage_model() short-circuiting on window plans, and included the verbatim /usage output. That made this quick to confirm. In Hermes, _render_usage_window_lines in agent/account_usage.py is the only place that prints the [bar] N% used gauge, and it puts the plan name on the first line, exactly as your parser expects. Your tests pass here, and the new window-layout test fails against the old parser while the account-limits one still passes. It's on main as 47e6f47.

I kept the plan-name list as you wrote it. Hermes has no fixed list of plan names, because the header is whatever plan name the Portal returns. A plan missing from the list behaves as it did before this change, with no Nous rows and no error. If you run into one, a comment with the header line is all I need to add it. I also kept reading <1% used as 0% used. Hermes prints <1% for any sliver above zero, so a card can show 100% left while a little has been used, which seems fine at this scale.

I don't have a window-plan account, so I couldn't check it in Desktop. Could you try it from main and tell me whether the four windows and their reset times match what /usage shows?

Catalog installs get it once the catalog pin is bumped upstream. Until then, you can copy plugin.js from main.

@jorisquintano

Copy link
Copy Markdown
Contributor Author

Thanks, and thanks for the fast merge.

The window-plan check you asked for — done, from main (47e6f47), on the account that reported the bug (Hermes Plus, usage-window plan, no monthly grant). The input is the gateway's own /usage text for that account, the output is the plugin's parseUsageOutput() → cardsFromUsageGroups() running in the repo's vm harness (node --test):

/usage (verbatim, live):

Hermes Plus
Today                   [||||||||||]  100% used  ·  resets in 23h
This week               [|||       ]  28% used  ·  resets in 6d 23h
This month              [|         ]  9% used  ·  resets in 4d 4h
Large models (monthly)  [|         ]  9% used  ·  resets in 4d 4h
Keep going on credits: on. Uses your credits when included usage runs out.
Top-up credits: $14.81
Renews: Nov 3, 2026

Cards the plugin builds from it:

window used remaining reset text
Today 100 0 in 23h
This week 28 72 in 6d 23h
This month 9 91 in 4d 4h
Large models (monthly) 9 91 in 4d 4h

Four windows, provider Nous Portal, reset text identical to the /usage line — and the same result through both distributions: plugin.js and catalog/desktop/plugin.js with HERMES_TEST_CATALOG=1. node --test test_quick_usage.cjs on the merged tree: 44/44.

What that does not prove: the Desktop pixels. My backend is headless, so I verified the card payload the page renders, not the rendered page. I'll report back here if the user sees anything off after reloading, since it's their account and their Desktop.

Two notes from deploying it:

  • The plan-name header is in the list — Hermes Plus exactly as the Portal returns it. I'll comment with the header line if another plan shows up unparsed.
  • Heads-up on the version string: main still says const VERSION = '0.2.22', and the signed updater only offers a release when newer(info.version, config.version). A catalog install at 0.2.26 that copies plugin.js from main therefore gets offered 0.2.26 — a release that still carries the old parser — and taking it silently reverts this fix. Locally I kept the merged code stamped 0.2.26 for that reason. A 0.2.27 release bump (or bumping VERSION on main) would close the hole for catalog users.

<1% used read as 0 used: agreed, at this scale it is fine, and it is the reading your tests lock in.

@jorisquintano

Copy link
Copy Markdown
Contributor Author

Follow-up from the reporter's Desktop (the account in the reproduction above), a few minutes after the merge.

Deployment + refresh, measured gateway-side. The Desktop plugin files were replaced with the merged build and the app reloaded; the gateway then recorded a fresh probe pass — cache/resetwatch/probe_snapshots.full.json rewritten at 11:21:11 and a second page fetch at 11:21:21 (Europe/Paris), against 11:02 for the previous one. That payload also shows the run is not provider-filtered ("disabled_providers": []), which is the state where the slash.exec usage path is the only source of Nous rows:

{"provider": "opencode-go", "plan": "Go", "windows": [
  {"label": "5h", "used_percent": 2.0, "remaining_percent": 98.0},
  {"label": "Weekly", "used_percent": 9.0, "remaining_percent": 91.0},
  {"label": "Monthly", "used_percent": 25.0, "remaining_percent": 75.0}],
 "disabled_providers": []}

The /usage text at that moment and the cards the merged parser builds from it (repo harness, parseUsageOutput → cardsFromUsageGroups):

Hermes Plus
Today                   [||||||||||]  100% used  ·  resets in 22h
This week               [|||       ]  28% used  ·  resets in 6d 22h
This month              [|         ]  9% used  ·  resets in 4d 4h
Large models (monthly)  [|         ]  9% used  ·  resets in 4d 4h
window used remaining reset text
Today 100 0 in 22h
This week 28 72 in 6d 22h
This month 9 91 in 4d 4h
Large models (monthly) 9 91 in 4d 4h

100% used in the Today window is a real state, not a parse artefact — the account is on keep going on credits and that window had just run out, so this also exercises the remaining: 0 edge of the gauge line.

What is measured and what is not: the gateway side is measured end to end (the text above comes from the same /usage producer the plugin reads, and the card payload comes from the merged code). The Desktop pixels are the reporter's — I will post the one-line visual confirmation here as soon as they report it, so this comment doesn't stand in for it.

Still worth a 0.2.27 bump for the reason in my previous comment: main stamps 0.2.22, so the in-app updater offers a 0.2.26 release that carries the old parser, and accepting it silently reverts this for anyone who copied plugin.js from main.

@jorisquintano

jorisquintano commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor Author

Visual confirmation from the Desktop, closing the loop on your question.

The Resetwatch page renders the four Nous Portal cards, and they match /usage row for row:

card shown reset shown
Today 0% left (in the low-balance red) Resets in 22h
This week 72% left Resets in 6d 22h
This month 91% left Resets in 4d 3h
Large models (monthly) 91% left Resets in 4d 3h

Two extras worth noting, since they came along for free:

  • The detail lines survive the window layout. The last card carries Keep going on credits: …, the top-up balance and Renews: Nov 3, 2026 — i.e. the non-gauge lines are still collected as details after the gauge parser takes the windows, and they land on the last window as before.
  • The countdowns are live, not frozen from my earlier snippet. My 11:20 measurement read in 4d 4h for the month windows; the screen, read a few minutes later, says in 4d 3h. That is the relative text decaying as expected, not a cached card.

No error banner, and opencode-go renders normally alongside (5h / Weekly / Monthly), so the fix adds the rows without disturbing the probe path.

I did not attach the screenshot: it carries the account's billing surface (plan, top-up balance, renewal), and the numbers already in this thread are enough to check the mapping.

So, from your side: nothing left to confirm — the window plan renders, the reset texts come from the same /usage line, and the <1% used reading you kept is what the red 0% left on a spent window relies on. Thanks again for the fast merge; I'll comment here if another plan header ever shows up unparsed.

@Adolanium

Copy link
Copy Markdown
Owner

Thanks @jorisquintano. Checking the parser against the live /usage text and then the rendered page, while keeping the billing screenshot out of the thread, is exactly the check I asked for. The 0% left on the spent Today window was the case I most wanted to see.

On the version string, the updater doesn't work that way here. The in-app check reads releases/latest, which is the standalone v0.2.18. Catalog releases are published with --latest=false, so a copy of plugin.js from main (0.2.22) is never offered 0.2.26 or any other catalog release. The catalog build doesn't ship the updater either, because catalog rule 3 forbids self-updating code, so catalog installs only move when the pin in hermes-agent moves.

Your fix is now in catalog-v0.2.27, with credit to you in the notes. I'm opening the re-pin in NousResearch/hermes-agent next. Catalog users get the fix once a maintainer merges that PR and they run hermes plugins update hermes-resetwatch. I'll link the PR here.

@Adolanium

Copy link
Copy Markdown
Owner

The catalog re-pin to 0.2.27 is open as NousResearch/hermes-agent#146839.

@jorisquintano

Copy link
Copy Markdown
Contributor Author

Correction to my previous comment, before it misleads anyone: the version-string warning I gave was wrong.

I assumed the in-app updater would offer 0.2.26 to a main-stamped install. It cannot: the signed updater reads this repo's GitHub Releases, and the latest one is still v0.2.18 (2026-09-17). Catalog builds ship as catalog-v0.2.x tags, which the updater does not read at all. So a plugin.js copied from main is never offered an older-parser release, and my "silently reverts the fix" claim does not apply. Sorry for the noise — the <1% used and header-list parts of that comment stand.

Related, and measurable: you cut catalog-v0.2.27 at 10:58Z today, 3 minutes after the merge. I installed that exact artifact here (it is byte-identical to the merged catalog build except VERSION = '0.2.27'), and re-ran the checks on the installed file:

  • node --test test_quick_usage.cjs → 44/44
  • /usage → cards through the installed parser: Today 0 left / 22h, This week 72 left / 6d 22h, This month 91 left / 4d 4h, Large models 91 left / 4d 4h

Hermes's catalog index still pins 0.2.26 (52b2d897), so hermes plugins check-updates reports "up to date" here and catalog installs pick up 0.2.27 when the index moves — nothing for you to do.

One thing from that comment is still true but harmless, so I'll restate it narrowly: main stamps VERSION = '0.2.22', the release commit does the bump. That only matters to whoever builds a package from main without bumping — not to users.

@Adolanium

Copy link
Copy Markdown
Owner

Thanks @jorisquintano for the correction, and for re-running the checks on the installed 0.2.27 build. 44/44 and matching cards from the release itself are good to have on the thread before the re-pin is reviewed.

You're right about main: it stays at 0.2.22 on purpose, and each release/catalog-* commit bumps the version, so nobody should build a package from main and ship it as is. Catalog installs move to 0.2.27 once NousResearch/hermes-agent#146839 is merged.

Adolanium added a commit to Iversondwx/hermes-resetwatch that referenced this pull request Nov 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants