personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add log age tracking to service manager

Track and display how long ago each service last logged output. Adds "Last"
column showing age in human-readable format (0m, 35m, 2h, 3d - never seconds)
to help quickly identify which services are actively logging vs. silent.

Stores timestamp with each log line when received and calculates age on render.
Uses same formatting style as existing uptime display for consistency.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

+31 -8
+31 -8
think/manage.py
··· 6 6 7 7 import argparse 8 8 import asyncio 9 - from datetime import timedelta 9 + from datetime import datetime, timedelta 10 10 11 11 import psutil 12 12 from blessed import Terminal ··· 27 27 self.running = True 28 28 self.term = Terminal() 29 29 self.status_message = "" 30 - self.last_log_lines = {} # Maps ref -> (stream, line) for most recent log 30 + self.last_log_lines = {} # Maps ref -> (timestamp, stream, line) for most recent log 31 31 self.cpu_cache = {} # Maps pid -> last cpu_percent value 32 32 self.cpu_procs = {} # Maps pid -> Process object for cpu tracking 33 33 ··· 89 89 line = message.get("line", "") 90 90 stream = message.get("stream", "stdout") 91 91 if ref: 92 - self.last_log_lines[ref] = (stream, line) 92 + self.last_log_lines[ref] = (datetime.now(), stream, line) 93 93 94 94 elif event == "exit": 95 95 # Clean up log lines for exited processes ··· 120 120 parts.append(f"{mins}m") 121 121 return " ".join(parts) 122 122 123 + def format_log_age(self, timestamp: datetime) -> str: 124 + """Format log timestamp age in human-readable format. 125 + 126 + Args: 127 + timestamp: When the log line was received 128 + 129 + Returns: 130 + Formatted string like "0m", "35m", "2h", "3d" (never seconds) 131 + """ 132 + delta = datetime.now() - timestamp 133 + total_seconds = int(delta.total_seconds()) 134 + 135 + if total_seconds < 60: 136 + return "0m" 137 + elif total_seconds < 3600: # Less than 1 hour 138 + return f"{total_seconds // 60}m" 139 + elif total_seconds < 86400: # Less than 1 day 140 + return f"{total_seconds // 3600}h" 141 + else: 142 + return f"{total_seconds // 86400}d" 143 + 123 144 def get_memory_mb(self, pid: int) -> str: 124 145 """Get process memory in MB, or '-' if unavailable. 125 146 ··· 168 189 output.append("") 169 190 170 191 # Table header 171 - header = f" {'Service':<15} {'PID':<8} {'Uptime':<12} {'MB':<8} {'%':<6} {'Last Log'}" 192 + header = f" {'Service':<15} {'PID':<8} {'Uptime':<12} {'MB':<8} {'%':<6} {'Last':<6} {'Log'}" 172 193 output.append(t.bold + header + t.normal) 173 194 output.append("─" * min(80, t.width)) 174 195 ··· 185 206 # Get log line for this service 186 207 log_display = "" 187 208 log_color = "" 209 + log_age = "" 188 210 if svc["ref"] in self.last_log_lines: 189 - stream, log_line = self.last_log_lines[svc["ref"]] 211 + timestamp, stream, log_line = self.last_log_lines[svc["ref"]] 212 + log_age = self.format_log_age(timestamp) 190 213 # Calculate available width: total - (fixed columns) 191 - # Fixed: "→ " (2) + name (15) + pid (8) + uptime (12) + memory (8) + cpu (6) + spaces (5) 192 - fixed_width = 56 214 + # Fixed: "→ " (2) + name (15) + pid (8) + uptime (12) + memory (8) + cpu (6) + age (6) + spaces (6) 215 + fixed_width = 63 193 216 available = max(0, t.width - fixed_width) 194 217 195 218 # Truncate log line if needed ··· 205 228 else: 206 229 log_color = t.normal 207 230 208 - line = f"{indicator} {name:<15} {pid:<8} {uptime:<12} {memory:>7} {cpu:>5} " 231 + line = f"{indicator} {name:<15} {pid:<8} {uptime:<12} {memory:>7} {cpu:>5} {log_age:>5} " 209 232 210 233 if i == self.selected: 211 234 output.append(t.black_on_white(line + log_display))