mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 10:36:35 +00:00
099d73dde8
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>
114 lines
5.1 KiB
Swift
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
|
|
)
|
|
}
|
|
}
|
|
}
|