[object Object]

← back to Ticket System

ticket bar: show swap / qwen-27b sessions / ollama reachability

73d9bb5178b06e206e69eaa583cb7a2103a0be33 · 2026-09-11 14:41:14 -0700 · Steve Abrams

Adds a health segment beside CPU: 'SWAP 97% · q27b 19 · oll ✓ m1 ✓'.
Swap is red >=90%, amber >=75% — it is the binding constraint behind TK-11519,
where local qwen inference collapsed (95s vs 0.32s on Mac1) with swap ~97% used.

Only instant signals are sampled: sysctl and pgrep, plus two reachability checks
hard-capped at 2s. Deliberately NOT an inference probe — /api/generate measured
95s on this box, and a status bar that samples that would freeze exactly when the
machine is worst, which is when you most need to read it.

Also fixes a pre-existing bug found on the way in: refreshSystem() was only called
once at launch, so the existing CPU readout went stale immediately and never
updated. It now runs on its own 15s timer alongside the 4s ticket refresh.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Files touched

Diff

commit 73d9bb5178b06e206e69eaa583cb7a2103a0be33
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Sep 11 14:41:14 2026 -0700

    ticket bar: show swap / qwen-27b sessions / ollama reachability
    
    Adds a health segment beside CPU: 'SWAP 97% · q27b 19 · oll ✓ m1 ✓'.
    Swap is red >=90%, amber >=75% — it is the binding constraint behind TK-11519,
    where local qwen inference collapsed (95s vs 0.32s on Mac1) with swap ~97% used.
    
    Only instant signals are sampled: sysctl and pgrep, plus two reachability checks
    hard-capped at 2s. Deliberately NOT an inference probe — /api/generate measured
    95s on this box, and a status bar that samples that would freeze exactly when the
    machine is worst, which is when you most need to read it.
    
    Also fixes a pre-existing bug found on the way in: refreshSystem() was only called
    once at launch, so the existing CPU readout went stale immediately and never
    updated. It now runs on its own 15s timer alongside the 4s ticket refresh.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---
 desktop-bar/TicketBar.swift | 46 ++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 45 insertions(+), 1 deletion(-)

diff --git a/desktop-bar/TicketBar.swift b/desktop-bar/TicketBar.swift
index 385c488d..e73da74a 100644
--- a/desktop-bar/TicketBar.swift
+++ b/desktop-bar/TicketBar.swift
@@ -74,6 +74,7 @@ private final class BarController: NSObject, NSApplicationDelegate, NSWindowDele
   private let working = NSTextField(labelWithString: "Working · —")
   private let latest = NSTextField(labelWithString: "Latest · —")
   private let cpuLabel = NSTextField(labelWithString: "CPU · —")
+  private let healthLabel = NSTextField(labelWithString: "SWAP · —")
   private var stack: NSStackView!
   private var tiles: [BarTile] = []
   private var openButton: NSButton!
@@ -95,6 +96,7 @@ private final class BarController: NSObject, NSApplicationDelegate, NSWindowDele
   private var latestDetail = "No activity yet"
   private var ticketPopup: NSWindow?
   private var timer: Timer?
+  private var systemTimer: Timer?
   private var dragMonitor: Any?
   private var draggingKey: String?
   private var dragStartX: CGFloat?
@@ -113,6 +115,9 @@ private final class BarController: NSObject, NSApplicationDelegate, NSWindowDele
     refresh()
     refreshSystem()
     timer = Timer.scheduledTimer(withTimeInterval: 4, repeats: true) { [weak self] _ in self?.refresh() }
+    // TK-11519: system health was only sampled at launch, so CPU went stale immediately.
+    // 15s is deliberate — every probe below is instant (sysctl/pgrep) or capped at 2s.
+    systemTimer = Timer.scheduledTimer(withTimeInterval: 15, repeats: true) { [weak self] _ in self?.refreshSystem() }
     NotificationCenter.default.addObserver(self, selector: #selector(reposition), name: NSApplication.didChangeScreenParametersNotification, object: nil)
   }
 
@@ -178,6 +183,11 @@ private final class BarController: NSObject, NSApplicationDelegate, NSWindowDele
     cpuLabel.lineBreakMode = .byTruncatingTail
     cpuLabel.alignment = .center
     cpuLabel.widthAnchor.constraint(equalToConstant: 112).isActive = true
+    healthLabel.font = .monospacedDigitSystemFont(ofSize: 12, weight: .medium)
+    healthLabel.lineBreakMode = .byTruncatingTail
+    healthLabel.alignment = .center
+    healthLabel.widthAnchor.constraint(equalToConstant: 236).isActive = true
+    healthLabel.toolTip = "swap used · qwen-27b sessions · local ollama · Mac1 ollama (TK-11519)"
     sniperButton = NSButton(title: "RAM Sniper · ON", target: self, action: #selector(toggleRamSniper))
     sniperButton.bezelStyle = .inline
     sniperButton.wantsLayer = true
@@ -185,7 +195,7 @@ private final class BarController: NSObject, NSApplicationDelegate, NSWindowDele
     sniperButton.layer?.borderWidth = 1
     sniperButton.toolTip = "Toggle the background CPU/RAM priority sniper"
     sniperButton.setContentHuggingPriority(.required, for: .horizontal)
-    stack = NSStackView(views: tiles + [cpuLabel, sniperButton, open, themeButton, posButton, dotsButton])
+    stack = NSStackView(views: tiles + [cpuLabel, healthLabel, sniperButton, open, themeButton, posButton, dotsButton])
     stack.orientation = .horizontal
     stack.spacing = 6
     stack.alignment = .centerY
@@ -597,8 +607,42 @@ private final class BarController: NSObject, NSApplicationDelegate, NSWindowDele
         DispatchQueue.main.async { self.flashSniperButton() }
         sniper = true
       }
+      // ---- TK-11519 health stats -------------------------------------------------
+      // ONLY cheap signals. A local /api/generate probe measured 95s while this box is
+      // swap-starved; a status bar must never block on that. sysctl/pgrep are instant and
+      // the two reachability checks are hard-capped at 2s.
+      var swapPct = -1.0
+      let swapRaw = self.runCommand("/usr/sbin/sysctl", ["-n", "vm.swapusage"])
+      // e.g. "total = 48128.00M  used = 46637.81M  free = 1490.19M"
+      let nums = swapRaw.split(separator: " ").compactMap { part -> Double? in
+        guard part.hasSuffix("M") else { return nil }
+        return Double(part.dropLast())
+      }
+      if nums.count >= 2, nums[0] > 0 { swapPct = nums[1] / nums[0] * 100 }
+      let swapText = swapPct < 0 ? "SWAP —" : String(format: "SWAP %.0f%%", swapPct)
+
+      let qwenCount = self.runCommand("/usr/bin/pgrep", ["-fc", "codex.*qwen3.8-27b-heretic"])
+        .trimmingCharacters(in: .whitespacesAndNewlines)
+      let qwenText = qwenCount.isEmpty ? "q27b 0" : "q27b \(qwenCount)"
+
+      // reachability only — NOT an inference test
+      func up(_ url: String) -> Bool {
+        let out = self.runCommand("/usr/bin/curl", ["-s", "-o", "/dev/null", "-m", "2", "-w", "%{http_code}", url])
+        return out.trimmingCharacters(in: .whitespacesAndNewlines) == "200"
+      }
+      let localUp = up("http://127.0.0.1:11434/api/tags")
+      let mac1Up  = up("http://192.168.1.133:11434/api/tags")
+      let healthText = "\(swapText) · \(qwenText) · oll \(localUp ? "✓" : "✗") m1 \(mac1Up ? "✓" : "✗")"
+      // red once swap is the binding constraint, amber approaching it
+      let healthColor: NSColor = swapPct >= 90 ? NSColor(calibratedRed: 1.0, green: 0.45, blue: 0.45, alpha: 1)
+                              : swapPct >= 75 ? NSColor(calibratedRed: 1.0, green: 0.80, blue: 0.40, alpha: 1)
+                              : NSColor(calibratedWhite: 0.78, alpha: 1)
+
       DispatchQueue.main.async {
         self.cpuLabel.stringValue = cpuText
+        self.healthLabel.stringValue = healthText
+        self.healthLabel.textColor = healthColor
+        self.healthLabel.toolTip = "swap \(swapText) · \(qwenText) qwen-27b sessions · local ollama \(localUp ? "up" : "DOWN") · Mac1 ollama \(mac1Up ? "up" : "DOWN")  (TK-11519)"
         self.sniperButton.title = sniper ? "RAM Sniper · ON" : "RAM Sniper · OFF"
         self.styleSniperButton(active: sniper)
       }

← 16c35905 auto-data-snapshot: 2026-09-11T14:39:38 (2 data files) — des  ·  back to Ticket System  ·  fix: async /api/skills scan + stale-while-revalidate to prev 1e215079 →