Files
scarf/scarf/scarf/Core/Services/NousSubscriptionService.swift
T
Alan Wizemann 099d73dde8 feat(scarfmon): instrument Nous model catalog + subscription path (beach-ball investigation)
User reported a remote-context beach-ball when opening the model
picker with Nous as the active provider. Existing measure points
showed loadProviders + loadModels at ~315ms each (fast). The
beach-ball must be in the uninstrumented Nous-overlay branch the
picker fires when nous is selected.

Adds four measure points covering every blocking call in that path:

- nous.subscription.loadState (interval, .diskIO) — auth.json read
  via NousSubscriptionService.loadState. Already known to do an SSH
  read; now precisely measurable.
- nous.readCache (interval, .diskIO) — nous_models cache read,
  TWO sequential SSH ops (fileExists + readFile).
- nous.bearerToken (interval, .diskIO) — auth.json read AGAIN inside
  fetchModels. **This is a duplicate read** — loadState already
  parsed the same file moments earlier. Comment-flagged as a
  caching candidate.
- nous.fetchModels (interval, .transport) + .bytes (event) — HTTP
  GET against the Nous /v1/models endpoint with the body byte count
  attached. The most likely beach-ball culprit if the endpoint is
  slow or hung.

After the next capture we'll know which of the four owns the user's
wall-clock; if `nous.bearerToken` shows up alongside
`nous.subscription.loadState` with similar duration, the duplicate
read is also a real cost worth fixing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 11:50:51 +02:00

114 lines
5.1 KiB
Swift

import Foundation
import os
import ScarfCore
/// Snapshot of the user's Nous Portal subscription state, derived from the
/// `providers.nous` entry in `~/.hermes/auth.json`. Read-only Scarf never
/// writes the subscription record; `hermes model` + `hermes auth` own that
/// path.
struct NousSubscriptionState: Sendable, Hashable {
/// True when `providers.nous` exists and has a usable access token.
/// Mirrors the `nous_auth_present` field on
/// `NousSubscriptionFeatures` in `hermes_cli/nous_subscription.py`.
let present: Bool
/// True when the user's **active provider** is `nous`, i.e. they've not
/// just authed but selected it as the primary model provider. The Tool
/// Gateway only routes tools when this is true auth alone isn't enough.
let providerIsNous: Bool
/// Last update time for the auth record, if known. Useful in the Health
/// view to tell the user when their subscription state was last refreshed.
let updatedAt: Date?
static let absent = NousSubscriptionState(present: false, providerIsNous: false, updatedAt: nil)
/// Overall subscription active for Tool Gateway routing. Both halves have
/// to line up: auth record present *and* `nous` is the active provider.
/// Mirrors `NousSubscriptionFeatures.subscribed` on the Python side.
var subscribed: Bool { present && providerIsNous }
/// Days since the auth record was last touched (refreshed by Hermes
/// or re-authed by the user). Hermes refreshes on every agent boot,
/// so a large value here means the user hasn't started a session
/// recently which is exactly when the refresh token is at risk
/// of expiring (typical ~30 day lifetime). Returns nil when
/// `updatedAt` is unknown (older Hermes versions). Capped at
/// `Int.max` to avoid overflow on absurd inputs.
func daysSinceLastRefresh(now: Date = Date()) -> Int? {
guard let updatedAt else { return nil }
let seconds = now.timeIntervalSince(updatedAt)
guard seconds > 0 else { return 0 }
return Int(seconds / 86_400)
}
/// True when we haven't seen a Hermes refresh in 14 days half
/// the typical 30-day Nous refresh-token lifetime. This is the
/// trigger for the "enable keepalive" nudge: still recoverable
/// (refresh token hasn't expired yet) but heading there. Returns
/// false when `updatedAt` is unknown we don't nudge on missing
/// data, only on confirmed staleness.
var hasStaleRefresh: Bool {
guard let days = daysSinceLastRefresh() else { return false }
return days >= 14
}
}
/// Reads `auth.json` to detect Nous Portal subscription state. Delegates file
/// I/O to the active `ServerTransport`, so remote installations work the same
/// as local ones.
///
/// The auth-record shape is defined by hermes-agent and is load-bearing. This
/// service parses a small, stable subset and tolerates anything new Hermes
/// adds we only rely on `providers.nous` being a dict with `access_token`
/// and `active_provider` being either `"nous"` or not.
struct NousSubscriptionService: Sendable {
private let logger = Logger(subsystem: "com.scarf", category: "NousSubscriptionService")
let authJSONPath: String
let transport: any ServerTransport
nonisolated init(context: ServerContext = .local) {
self.authJSONPath = context.paths.authJSON
self.transport = context.makeTransport()
}
/// Escape hatch for tests point at a fixture `auth.json` without
/// constructing a full `ServerContext`. Uses `LocalTransport` so the
/// fixture must live on the local filesystem.
init(path: String) {
self.authJSONPath = path
self.transport = LocalTransport()
}
/// Load the current subscription state. Returns ``NousSubscriptionState/absent``
/// on any read or parse failure callers treat "absent" and "can't
/// read" the same in UI (show a "not subscribed" CTA).
nonisolated func loadState() -> NousSubscriptionState {
ScarfMon.measure(.diskIO, "nous.subscription.loadState") {
guard let data = try? transport.readFile(authJSONPath) else {
return .absent
}
guard let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
logger.warning("auth.json is not a JSON object; assuming no Nous subscription")
return .absent
}
let providers = root["providers"] as? [String: Any] ?? [:]
let nous = providers["nous"] as? [String: Any]
let token = nous?["access_token"] as? String
let present = (token?.isEmpty == false)
let activeProvider = root["active_provider"] as? String
let providerIsNous = (activeProvider == "nous")
let updatedAt: Date? = {
guard let raw = root["updated_at"] as? String else { return nil }
return ISO8601DateFormatter().date(from: raw)
}()
return NousSubscriptionState(
present: present,
providerIsNous: providerIsNous,
updatedAt: updatedAt
)
}
}
}