Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 50 additions & 2 deletions Sources/CodeCaps/MonitorModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1788,7 +1788,9 @@ final class MonitorModel: ObservableObject {
} else if self.skipsSnapshotIOForTesting {
self.handoffError = nil
} else if self.localEnabled && local != nil {
try LocalQuotaSnapshot.write(windows: self.localWindows, issues: self.issues, customMarks: exportedCustomMarks(), now: self.now)
try LocalQuotaSnapshot.write(windows: self.localWindows, issues: self.issues,
customMarks: exportedCustomMarks(),
alerts: exportedAlerts(now: self.now), now: self.now)
}
else { try LocalQuotaSnapshot.remove() }
self.handoffError = nil
Comment on lines 1788 to 1796

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Bug high

refreshRunawayUsageState(recordSamples:) runs after LocalQuotaSnapshot.write in refresh() (~line 1791) and rebuildLocalState() (~line 1968), so the snapshot ships the previous refresh's activeRunawayAnomalies instead of the current set. Newly-emerged runaway alerts arrive one poll cycle late on the iOS companion and resolved alerts persist an extra cycle, violating the "raise once" dedupe contract; move refreshRunawayUsageState immediately before the LocalQuotaSnapshot.write call in both locations.

Prompt for LLM

File Sources/CodeCaps/MonitorModel.swift:

Line 1788 to 1796:

refreshRunawayUsageState(recordSamples:) runs after LocalQuotaSnapshot.write in refresh() (~line 1791) and rebuildLocalState() (~line 1968), so the snapshot ships the previous refresh's activeRunawayAnomalies instead of the current set. Newly-emerged runaway alerts arrive one poll cycle late on the iOS companion and resolved alerts persist an extra cycle, violating the "raise once" dedupe contract; move refreshRunawayUsageState immediately before the LocalQuotaSnapshot.write call in both locations.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

​

​

Expand Down Expand Up @@ -1964,7 +1966,8 @@ final class MonitorModel: ObservableObject {
do {
if localEnabled {
try LocalQuotaSnapshot.write(windows: localWindows, issues: issues,
customMarks: exportedCustomMarks(), now: now)
customMarks: exportedCustomMarks(),
alerts: exportedAlerts(now: now), now: now)
} else {
try LocalQuotaSnapshot.remove()
}
Expand Down Expand Up @@ -2175,3 +2178,48 @@ func resetCountdown(_ reset: Date?, now: Date) -> String {
if minutes >= 60 { return "Resets in \(minutes / 60)h \(minutes % 60)m" }
return "Resets in \(minutes)m"
}

// MARK: - Alert export for the iOS companion

extension MonitorModel {
/// The alerts the companion should raise, in the same shape it reads.
///
/// Owner request 2026-10-08: "iOS should have info about heavy anomalies in
/// usage or runaway usage like the mac." The decision is made **here** and
/// shipped as data, rather than recomputed on the phone, for two reasons.
///
/// The rules need this Mac's burn-rate history — a per-machine file of
/// measured hourly intervals built from local readings. The companion has
/// no access to it and could not reproduce the comparison, so a phone-side
/// implementation would either alert on much weaker evidence or not at all.
///
/// And the identity matters more than the text: if each surface evaluated
/// its own thresholds they could disagree about whether something was
/// anomalous, which is worse than one surface being silent. One decision,
/// two notifications.
///
/// `id` is stable for a given anomaly so the companion can raise it once
/// rather than once per poll — this payload is rewritten on every refresh.
func exportedAlerts(now: Date = Date()) -> [LocalQuotaSnapshot.AlertPayload] {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Bug high

exportedAlerts only emits "runaway" AlertPayload entries and ignores alarmManager.recentEvents, silently dropping the "reset" and "vendorReset" kinds that LocalQuotaSnapshot.AlertPayload.kind and WireAlert document as supported. Extend exportedAlerts to walk alarmManager.recentEvents and emit an AlertPayload with kind isMidWindow ? "vendorReset" : "reset" for each unacknowledged ResetAlarmEvent using a stable id like reset|(providerId)|(windowLabel)|(endedPeriodResetAt).

func exportedAlerts(now: Date = Date()) -> [LocalQuotaSnapshot.AlertPayload] {
    var out: [LocalQuotaSnapshot.AlertPayload] = []
    let formatter = ISO8601DateFormatter()

    // Runaway usage currently burning far faster than the owner's own
    // pattern.  Only the live set, so a resolved anomaly stops being
    // re-offered on the next poll.
    for anomaly in activeRunawayAnomalies {
        let label = AnomalyDetector.windowLabel(windowId: anomaly.windowId)
        let title = displaySections.first { $0.id == anomaly.providerKey }?.title
            ?? anomaly.providerKey
        out.append(LocalQuotaSnapshot.AlertPayload(
            id: "runaway|\(anomaly.providerKey)|\(anomaly.windowId)|\(anomaly.kind.rawValue)",
            kind: "runaway",
            providerKey: anomaly.providerKey,
            providerTitle: title,
            windowLabel: label,
            summary: anomaly.summary,
            occurredAt: formatter.string(from: anomaly.observedAt ?? now)))
    }

    // Courtesy / vendor resets the tracker decided to raise (this PR).
    // Without these the iOS companion would never learn a reset happened,
    // which is the whole reason the Mac-side rule was added.
    for event in alarmManager.recentEvents where !event.isAcknowledged {
        let label = event.windowLabel
        let title = displaySections.first { $0.id == event.providerId }?.title
            ?? event.providerId
        out.append(LocalQuotaSnapshot.AlertPayload(
            id: "\(event.isVendorReset ? "vendorReset" : "reset")|\(event.providerId)|\(event.windowLabel)|\(event.endedPeriodResetAt.timeIntervalSince1970)",
            kind: event.isVendorReset ? "vendorReset" : "reset",
            providerKey: event.providerId,
            providerTitle: title,
            windowLabel: label,
            summary: event.summary ?? "Quota was reset.",
            occurredAt: formatter.string(from: event.endedPeriodResetAt)))
    }
    return out
}
Prompt for LLM

File Sources/CodeCaps/MonitorModel.swift:

Line 2203:

exportedAlerts only emits "runaway" AlertPayload entries and ignores alarmManager.recentEvents, silently dropping the "reset" and "vendorReset" kinds that LocalQuotaSnapshot.AlertPayload.kind and WireAlert document as supported. Extend exportedAlerts to walk alarmManager.recentEvents and emit an AlertPayload with kind isMidWindow ? "vendorReset" : "reset" for each unacknowledged ResetAlarmEvent using a stable id like reset|\(providerId)|\(windowLabel)|\(endedPeriodResetAt).

Suggested Code:

    func exportedAlerts(now: Date = Date()) -> [LocalQuotaSnapshot.AlertPayload] {
        var out: [LocalQuotaSnapshot.AlertPayload] = []
        let formatter = ISO8601DateFormatter()

        // Runaway usage currently burning far faster than the owner's own
        // pattern.  Only the live set, so a resolved anomaly stops being
        // re-offered on the next poll.
        for anomaly in activeRunawayAnomalies {
            let label = AnomalyDetector.windowLabel(windowId: anomaly.windowId)
            let title = displaySections.first { $0.id == anomaly.providerKey }?.title
                ?? anomaly.providerKey
            out.append(LocalQuotaSnapshot.AlertPayload(
                id: "runaway|\(anomaly.providerKey)|\(anomaly.windowId)|\(anomaly.kind.rawValue)",
                kind: "runaway",
                providerKey: anomaly.providerKey,
                providerTitle: title,
                windowLabel: label,
                summary: anomaly.summary,
                occurredAt: formatter.string(from: anomaly.observedAt ?? now)))
        }

        // Courtesy / vendor resets the tracker decided to raise (this PR).
        // Without these the iOS companion would never learn a reset happened,
        // which is the whole reason the Mac-side rule was added.
        for event in alarmManager.recentEvents where !event.isAcknowledged {
            let label = event.windowLabel
            let title = displaySections.first { $0.id == event.providerId }?.title
                ?? event.providerId
            out.append(LocalQuotaSnapshot.AlertPayload(
                id: "\(event.isVendorReset ? "vendorReset" : "reset")|\(event.providerId)|\(event.windowLabel)|\(event.endedPeriodResetAt.timeIntervalSince1970)",
                kind: event.isVendorReset ? "vendorReset" : "reset",
                providerKey: event.providerId,
                providerTitle: title,
                windowLabel: label,
                summary: event.summary ?? "Quota was reset.",
                occurredAt: formatter.string(from: event.endedPeriodResetAt)))
        }
        return out
    }

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

​

​

var out: [LocalQuotaSnapshot.AlertPayload] = []
let formatter = ISO8601DateFormatter()

// Runaway usage currently burning far faster than the owner's own
// pattern. Only the live set, so a resolved anomaly stops being
// re-offered on the next poll.
for anomaly in activeRunawayAnomalies {
let label = AnomalyDetector.windowLabel(windowId: anomaly.windowId)
let title = displaySections.first { $0.id == anomaly.providerKey }?.title
?? anomaly.providerKey
out.append(LocalQuotaSnapshot.AlertPayload(
id: "runaway|\(anomaly.providerKey)|\(anomaly.windowId)|\(anomaly.kind.rawValue)",
kind: "runaway",
providerKey: anomaly.providerKey,
providerTitle: title,
windowLabel: label,
summary: anomaly.summary,
occurredAt: formatter.string(from: anomaly.observedAt ?? now)))
}
return out
}
}
19 changes: 16 additions & 3 deletions Sources/CodeCaps/PipWidget.swift
Original file line number Diff line number Diff line change
Expand Up @@ -311,8 +311,8 @@ final class PipWidgetController: NSObject, NSWindowDelegate {
// and its frame overlaps the console.
p.level = .floating
p.isMovableByWindowBackground = true
p.titleVisibility = .hidden
p.titlebarAppearsTransparent = true
// Borderless panel: the rounded SwiftUI surface is the whole window, so
// there is no titlebar strip to keep invisible. See `panelStyleMask`.
p.isOpaque = false
p.backgroundColor = .clear
p.hasShadow = true
Expand Down Expand Up @@ -408,8 +408,21 @@ final class PipWidgetController: NSObject, NSWindowDelegate {
extension PipMetrics {
/// `.resizable` is what turns this from a fixed HUD into a window the owner
/// can drag the edge of; the view inside degrades rather than clips.
///
/// There is deliberately **no `.titled` and no `.hudWindow`** (owner report,
/// 2026-10-08: "it needs to not have the blank silver part on top for no
/// reason"). Both of those make AppKit paint a titlebar strip at the top
/// of the window that sits *outside* the SwiftUI content, so it shows as a
/// blank light band above the rounded panel — and with `titleVisibility`
/// hidden and `titlebarAppearsTransparent` set, it was a strip with nothing
/// on it at all. `.fullSizeContentView` cannot hide it either, because the
/// strip is drawn by the window frame rather than by the content view.
///
/// A borderless, nonactivating, resizable panel has no such strip: the
/// rounded panel *is* the window. `isMovableByWindowBackground` already
/// makes the body draggable, so the titlebar was never load-bearing.
static var panelStyleMask: NSWindow.StyleMask {
[.titled, .nonactivatingPanel, .fullSizeContentView, .utilityWindow, .hudWindow, .resizable]
[.borderless, .nonactivatingPanel, .resizable]
}
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/QuotaCore/AnomalyDetector.swift
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ public struct AnomalyDetector: Sendable {
return "\(prefix) \(window): \(String(format: "%.1f", ratio))× \(comparison)."
}

static func windowLabel(windowId: String) -> String {
public static func windowLabel(windowId: String) -> String {
if let range = windowId.range(of: ":", options: .backwards) {
return String(windowId[range.upperBound...])
}
Expand Down
59 changes: 57 additions & 2 deletions Sources/QuotaCore/LocalQuotaSnapshot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,21 @@ public enum LocalQuotaSnapshot {
public let windows: [QuotaWindow]
public let issues: [String: String]?
public let customMarks: [String: CustomMarkPayload]?
/// Runaway and reset alerts the Mac already decided to raise, so the
/// companion can raise the same ones.
///
/// Owner request 2026-10-08: "iOS should have info about heavy
/// anomalies in usage or runaway usage like the mac." The Mac computes
/// these from its own burn-rate history, which iOS has no access to and
/// could not reproduce: the history is a per-Mac file of measured
/// hourly intervals. Shipping the *decision* rather than the raw
/// samples keeps one implementation of the rules, and means the two
/// surfaces cannot disagree about whether something was anomalous.
///
/// Optional and additive for the same reason `issues` is: an older
/// build's file still parses. A consumer that sees no `alerts` simply
/// has nothing to raise.
public let alerts: [AlertPayload]?

public init(
format: String,
Expand All @@ -48,7 +63,8 @@ public enum LocalQuotaSnapshot {
generatedAt: String,
windows: [QuotaWindow],
issues: [String: String]? = nil,
customMarks: [String: CustomMarkPayload]? = nil
customMarks: [String: CustomMarkPayload]? = nil,
alerts: [AlertPayload]? = nil
) {
self.format = format
self.version = version
Expand All @@ -57,6 +73,43 @@ public enum LocalQuotaSnapshot {
self.windows = windows
self.issues = issues
self.customMarks = customMarks
self.alerts = alerts
}
}

/// One alert the Mac has already decided is worth raising.
public struct AlertPayload: Codable, Equatable, Sendable {
/// Stable identity, so the companion can raise each alert once rather
/// than once per poll: the Mac rewrites this file every refresh.
public let id: String
/// `runaway`, `reset`, or `vendorReset`. A raw string rather than an
/// enum so a newer Mac writing an unknown kind degrades to "something
/// happened" on an older companion instead of failing to decode.
public let kind: String
public let providerKey: String
public let providerTitle: String
public let windowLabel: String
/// Owner-facing sentence, generated by the same code the Mac shows.
public let summary: String
/// When the alert happened, ISO 8601.
public let occurredAt: String

public init(
id: String,
kind: String,
providerKey: String,
providerTitle: String,
windowLabel: String,
summary: String,
occurredAt: String
) {
self.id = id
self.kind = kind
self.providerKey = providerKey
self.providerTitle = providerTitle
self.windowLabel = windowLabel
self.summary = summary
self.occurredAt = occurredAt
}
}

Expand All @@ -75,6 +128,7 @@ public enum LocalQuotaSnapshot {
windows: [QuotaWindow],
issues: [String: String] = [:],
customMarks: [String: CustomMarkPayload]? = nil,
alerts: [AlertPayload]? = nil,
now: Date = Date(),
to url: URL = destination()
) throws {
Expand All @@ -86,7 +140,8 @@ public enum LocalQuotaSnapshot {
generatedAt: ISO8601DateFormatter().string(from: now),
windows: windows.map { $0.normalizedForExport() },
issues: published.isEmpty ? nil : published,
customMarks: customMarks)
customMarks: customMarks,
alerts: (alerts?.isEmpty ?? true) ? nil : alerts)
let data = try JSONEncoder().encode(payload)
guard data.count <= 10_485_760 else { throw CocoaError(.fileWriteOutOfSpace) }
try writePrivately(data, to: url, in: directory, using: manager)
Expand Down
61 changes: 61 additions & 0 deletions Sources/QuotaCore/ResetAlarmTracker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,22 @@ public enum ResetAlarmPolicy {
/// cannot regain quota inside its own period, so the size of the rise is
/// the evidence and 30 points is far beyond rounding or a stale reading.
public static let vendorRestoreRise: Double = 30

/// A courtesy reset can land on any level, so "what did it come back to" is
/// the wrong question. What is reliable is the shape: between two
/// consecutive readings, quota goes from mostly spent to nearly untouched.
///
/// Owner request 2026-10-08 — a reset may be performed by the vendor, or by
/// the owner using a reset the provider offers, and either one must alert.
/// Missing it means working against a cap that no longer exists.
///
/// The thresholds are set by what quota can legitimately do *within* a
/// period, which is only fall. A rise from at-or-below 80% to at-or-above
/// 95% is 15 points and cannot happen without a reset; the existing
/// `surgeMidWindow` rule already uses this shape, but only while the
/// window's own end is still in the future, which is not the owner's case.
public static let courtesyRestoreCeiling: Double = 80
public static let courtesyRestoreFloor: Double = 95
/// Windows and fires not seen for this long are forgotten.
public static let retention: TimeInterval = 45 * 86_400
/// A window's period counts toward "largest" for this long after it was
Expand Down Expand Up @@ -427,6 +443,22 @@ public struct ResetAlarmTracker: Sendable {
}
if jump > required || (jump > ResetAlarmPolicy.resetDriftTolerance && climbed) {
isReset = true
// When the quota also came back from mostly spent to nearly
// untouched, and the window had *not* reached its own end,
// this is a restore rather than an ordinary rollover.
//
// The `!hasPassed` guard is load-bearing. A window that
// simply reached the end of its period also goes from
// spent to full, and the owner ruled (2026-09-30) that a
// smaller window stays quiet unless it came near its cap.
// Labelling that "mid window" would bypass near-cap and
// alert on every ordinary rollover. A reset before the
// scheduled end is the case the owner cannot otherwise see
// (2026-10-08), and that is exactly what this catches.
if !hasPassed, let reading, let last = window.lastRemaining {
isMidWindow = reading >= ResetAlarmPolicy.courtesyRestoreFloor
&& last <= ResetAlarmPolicy.courtesyRestoreCeiling
}
nextResetAt = reported
} else if jump > -ResetAlarmPolicy.resetDriftTolerance || reported > readAt {
// Jitter, or a correction to a reset still ahead: follow it
Expand Down Expand Up @@ -467,6 +499,35 @@ public struct ResetAlarmTracker: Sendable {
isReset = true
nextResetAt = observation.resetAt.flatMap { $0 > readAt ? $0 : nil }
}
// A reset that lands before the window's own end: a courtesy reset the
// owner performed, or a vendor one, where the quota goes from
// mostly spent to nearly untouched between two consecutive checks.
//
// Owner request 2026-10-08: "can't the app tell if usage suddenly
// jumps to almost 100% available between 2 checks that there was a
// reset, whether done by the system or by a reset the user was
// allowed to optionally use? Please alert for that since if you
// don't know it was reset and they do a courtesy reset then you can
// start using again."
//
// The answer has to be yes: the consequence of missing one is
// working against a cap that no longer exists.
//
// `!hasPassed` is what keeps this from being a false-positive
// machine. A window that simply reached the end of its period also
// goes from spent to full, and the owner's standing rule is that a
// smaller window stays quiet unless it came near its cap — so the
// scheduled rollover must not be caught here. What this catches is
// the case that is otherwise invisible: quota restored *before* the
// window was due to end, which no amount of near-cap reasoning can
// distinguish from "nothing happened".
if !isReset, !hasPassed, let reading, let last = window.lastRemaining,
reading >= ResetAlarmPolicy.courtesyRestoreFloor,
last <= ResetAlarmPolicy.courtesyRestoreCeiling {
isReset = true
isMidWindow = true
nextResetAt = observation.resetAt.flatMap { $0 > readAt ? $0 : nil } ?? window.periodResetAt
}
// Mid-window reset: a quota that jumps back up while the period
// is still running. Three ways to see it, and none of them
// requires the quota to land at 100%:
Expand Down
14 changes: 13 additions & 1 deletion Tests/CodeCapsTests/PipMetricsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,19 @@ final class PipMetricsTests: XCTestCase {
let mask = PipMetrics.panelStyleMask
XCTAssertTrue(mask.contains(.resizable),
"The owner asked for a resizable window; the style mask is what grants it")
XCTAssertTrue(mask.contains(.fullSizeContentView))
// Owner report 2026-10-08: a blank silver strip sat along the top of the
// HUD "for no reason". `.titled` and `.hudWindow` both make AppKit paint
// a titlebar strip that lives outside the SwiftUI content, and no amount
// of `titleVisibility` or `fullSizeContentView` removes it — the strip
// belongs to the window frame. A borderless panel has no strip at all.
XCTAssertFalse(mask.contains(.titled),
"`.titled` brings back the blank silver strip the owner asked to remove")
XCTAssertFalse(mask.contains(.hudWindow),
"`.hudWindow` paints a titlebar strip outside the content view")
XCTAssertTrue(mask.contains(.borderless),
"the rounded panel is now the whole window, so it must be borderless")
XCTAssertTrue(mask.contains(.nonactivatingPanel),
"the HUD must still not steal focus from the app the owner is using")
XCTAssertLessThan(PipMetrics.minWidth, PipMetrics.maxWidth)
XCTAssertLessThan(PipMetrics.minHeight, PipMetrics.maxHeight)
}
Expand Down
Loading
Loading