Repository navigation
feat(alerts): courtesy-reset detection, iOS runaway alerts, and no PiP titlebar strip #195
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -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() | ||
| } | ||
|
|
@@ -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] { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 LLMTalk 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 | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
Talk to Kody by mentioning @kody
Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.