Skip to content

Cursor: pooledLimit now returned as a JSON string, so every quota fetch fails ("cannot unmarshal string into ... of type int") #151

Closed

Description

@daisylovekris

What happens

Cursor quota fetching fails on every poll tick, and the Cursor provider never appears in the menubar or dashboard at all.

level=ERROR msg="Failed to fetch Cursor quotas" error="cursor: fetch usage: json: cannot unmarshal string into Go struct field CursorSpendLimitUsage.spendLimitUsage.pooledLimit of type int"

Environment

  • onWatch v2.14.11 (also reproduced on v2.14.8)
  • macOS 14.8.5, arm64
  • Cursor source: default Connect RPC usage endpoint

Cause (from the error, and the source)

internal/api/cursor_types.go currently declares:

type CursorSpendLimitUsage struct {
	TotalSpend      int `json:"totalSpend"`
	PooledLimit     int `json:"pooledLimit"`
	PooledUsed      int `json:"pooledUsed"`
	PooledRemaining int `json:"pooledRemaining"`
	IndividualLimit int `json:"individualLimit"`
	...
}

Cursor's usage response now returns pooledLimit as a JSON string (e.g. "50000") instead of a number. encoding/json then fails the whole decode and the fetch is abandoned.

Relevant: docs/CURSOR_SETUP.md says pooledLimit present ⇒ Team account, so this may be Team-plan specific rather than universal.

Impact

  • Failing continuously since at least 2026-10-03 — 48,607 error lines accumulated so far (one per ~30s tick)
  • Cursor is dropped from the provider list entirely; there is no stale-value fallback, so a user loses visibility of that provider with no indication why
  • Because the entire decode aborts on one mismatched field, the other perfectly valid fields in the same response (totalSpend, pooledUsed, usage/plan info) are discarded too

Suggested fix

  1. Decode into a permissive type: either json.Number, or a small helper type with a custom UnmarshalJSON that accepts both 50000 and "50000". Apply it to all the numeric spend fields (totalSpend, pooledLimit, pooledUsed, pooledRemaining, individualLimit) since vendor-side int→string drift tends to arrive as a group.
  2. Consider retrying the decode field-by-field (or into map[string]json.RawMessage) so one drifted field degrades that field instead of the whole provider.
  3. Keep the last known-good values and mark the provider stale rather than removing it, so the failure is visible in the UI.

Repro

With a Cursor credential configured, every poll tick logs the error above and no Cursor card is rendered.

I can't share the raw response body (it carries account data), but the type mismatch is unambiguous from the unmarshal error. Happy to test a patched build if useful.

Activity

  1. daisylovekris commented on Oct 20, 2026

    @daisylovekris
    Author

    Adding a data point after upgrading to v2.14.13: still reproduces on the latest release, so this is not fixed by 2.14.12/2.14.13.

    • Latest occurrence: 2026-10-20T21:47:20+08:00, i.e. the first Cursor poll after the daemon restarted on 2.14.13
    • Cumulative error lines on one machine: 48,928 (up from 48,607 when the issue was filed earlier today)

    One correction to my original report: I assumed this might be Team-plan specific because docs/CURSOR_SETUP.md associates pooledLimit with Team accounts. The log on a free plan also shows the field being returned, as a string:

    msg="cursor: auth detected from SQLite" has_access_token=true has_refresh_token=true membership=free
    

    so the field is present (and string-typed) outside the Team path too — worth making the decode tolerant regardless of plan rather than special-casing Team.

  2. added a commit that references this issue on Oct 22, 2026
  3. prakersh commented on Oct 22, 2026

    @prakersh
    Contributor

    Thanks for the detailed report and the exact error line, that made this quick to pin down.

    This is fixed in #153, which is now merged into main. The Cursor decoder accepts numeric fields as plain numbers, quoted numbers like "50000", empty strings or null, so a format change like this no longer fails the whole poll. If one field still can't be read, only that field is affected: a non-quota field falls back to zero, and a quota whose limit or percent is unreadable is skipped for that poll, so the last good value stays on screen instead of a fake 0%.

    We also stopped treating pooledLimit as proof of a Team plan. Team detection now comes from the plan name, and onWatch remembers the last known plan if the plan lookup fails, so a Team account won't flip to Individual for a poll.

    It ships in v2.14.14. After upgrading, the Cursor card should come back on the first poll. If it doesn't, please share the log lines around "Cursor" and we'll take another look.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions