personal memory agent
0
fork

Configure Feed

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

Remove file growth health check from observers

Static screens with VP8 encoding may not produce detectable file writes
for 30 seconds (matching keyframe interval at 1fps), causing false positive
stall detection. Process health checks (is_healthy/is_running) already
catch actual capture failures.

- Remove STALL_THRESHOLD_CHUNKS, files_growing, stalled_chunks tracking
- Remove last_screencast_sizes/last_video_sizes dictionaries
- Remove file growth check blocks from main_loop()
- Remove files_growing from status event payloads
- Update CALLOSUM.md and DOCTOR.md docs

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

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

+4 -87
+1 -2
docs/CALLOSUM.md
··· 58 58 **Fields:** 59 59 - `status`: Periodic state (every 5s while running) 60 60 - From `observer.py`: `screencast`, `audio`, `activity` - Live capture state (for UI/debugging) 61 - - `screencast.files_growing` - Whether recording files are actively being written 62 61 - From `sense.py`: `describe`, `transcribe` - Processing pipeline state (with `running`/`queued` sub-fields) 63 62 - `observing`: `day`, `segment`, `files` - Recording window boundary crossed with saved files 64 63 - Remote events include `remote` (remote name) from `apps/remote/routes.py` ··· 69 68 - Remote events include `remote` (remote name) 70 69 - Observer events (`status`, `observing`) include `host` (hostname) and `platform` ("linux"/"darwin") for multi-host support 71 70 **Purpose:** Track observation pipeline from live capture state through processing completion 72 - **Health Model:** Fail-fast - observers exit if capture stalls (e.g., files not growing). Supervisor checks event freshness only. 71 + **Health Model:** Fail-fast - observers exit if capture process dies. Supervisor checks event freshness only. 73 72 **Path Format:** Relative to `JOURNAL_PATH` (e.g., `20251102/163045_300_center_DP-3_screen.webm` for multi-monitor recordings) 74 73 **Correlation:** `detected.ref` matches `logs.exec.ref` for the same handler process; `observed.segment` groups all files from same capture window 75 74 **Event Log:** Any observe event with `day` + `segment` fields is logged to `<day>/<segment>/events.jsonl` by supervisor (if directory exists)
+1 -1
docs/DOCTOR.md
··· 87 87 88 88 Services emit periodic status to Callosum (every 5 seconds when active): 89 89 90 - - `observe.status` - Capture state (screencast, audio, activity, files_growing) 90 + - `observe.status` - Capture state (screencast, audio, activity) 91 91 - `cortex.status` - Running agents list 92 92 - `supervisor.status` - Service health, stale heartbeats 93 93
+1 -43
observe/linux/observer.py
··· 49 49 RMS_THRESHOLD = 0.01 50 50 MIN_HITS_FOR_SAVE = 3 51 51 CHUNK_DURATION = 5 # seconds 52 - STALL_THRESHOLD_CHUNKS = 6 # Exit after this many chunks with no file growth 53 52 54 53 55 54 # Capture modes ··· 86 85 87 86 # Multi-file screencast tracking 88 87 self.current_streams: list[StreamInfo] = [] 89 - self.last_screencast_sizes: dict[str, int] = {} 90 88 91 89 # Tmux capture tracking 92 90 self.tmux_captures: list[dict] = [] ··· 104 102 # Mute state at segment start (determines save format) 105 103 self.segment_is_muted = False 106 104 107 - # Health tracking - whether screencast files are actively growing 108 - self.files_growing = False 109 - self.stalled_chunks = ( 110 - 0 # Consecutive chunks with no file growth in screencast mode 111 - ) 112 - 113 105 async def setup(self): 114 106 """Initialize audio devices and DBus connection.""" 115 107 # Detect and start audio recorder ··· 293 285 logger.info("Stopping previous screencast") 294 286 stopped_streams = await self.screencaster.stop() 295 287 self.current_streams = [] 296 - self.last_screencast_sizes = {} 297 - self.stalled_chunks = 0 298 288 299 289 # Collect screen filenames (files are already in draft dir with final names) 300 290 screen_files = [stream.filename for stream in stopped_streams] ··· 454 444 raise RuntimeError("No streams available") 455 445 456 446 self.current_streams = streams 457 - self.last_screencast_sizes = {s.file_path: 0 for s in streams} 458 - self.stalled_chunks = 0 459 447 460 448 logger.info(f"Started screencast with {len(streams)} stream(s)") 461 449 for stream in streams: ··· 517 505 "recording": True, 518 506 "streams": streams_info, 519 507 "window_elapsed_seconds": elapsed, 520 - "files_growing": self.files_growing, 521 508 } 522 509 else: 523 - screencast_info = {"recording": False, "files_growing": False} 510 + screencast_info = {"recording": False} 524 511 525 512 # Calculate tmux info 526 513 if self.current_mode == MODE_TMUX: ··· 615 602 616 603 # Files are already in draft folder, will be finalized at next boundary 617 604 self.current_streams = [] 618 - self.last_screencast_sizes = {} 619 - self.stalled_chunks = 0 620 605 # Force recalculate mode without screencast 621 606 self.current_mode = MODE_IDLE 622 607 ··· 673 658 f"hits={self.threshold_hits}/{MIN_HITS_FOR_SAVE}" 674 659 ) 675 660 await self.handle_boundary(new_mode) 676 - 677 - # Check if screencast files are actively growing (for health reporting) 678 - if self.current_mode == MODE_SCREENCAST and self.current_streams: 679 - any_growing = False 680 - for stream in self.current_streams: 681 - if os.path.exists(stream.file_path): 682 - current_size = os.path.getsize(stream.file_path) 683 - last_size = self.last_screencast_sizes.get(stream.file_path, 0) 684 - if current_size > last_size: 685 - any_growing = True 686 - self.last_screencast_sizes[stream.file_path] = current_size 687 - self.files_growing = any_growing 688 - 689 - # Fail-fast: exit if screencast stalled (files not growing) 690 - if any_growing: 691 - self.stalled_chunks = 0 692 - else: 693 - self.stalled_chunks += 1 694 - if self.stalled_chunks >= STALL_THRESHOLD_CHUNKS: 695 - logger.error( 696 - f"Screencast stalled for {self.stalled_chunks} chunks " 697 - f"({self.stalled_chunks * CHUNK_DURATION}s), exiting" 698 - ) 699 - self.running = False 700 - else: 701 - self.files_growing = False 702 - self.stalled_chunks = 0 703 661 704 662 # Emit status event 705 663 self.emit_status()
+1 -41
observe/macos/observer.py
··· 41 41 RMS_THRESHOLD = 0.01 42 42 MIN_HITS_FOR_SAVE = 3 43 43 SAMPLE_RATE = 48000 # Standard audio sample rate 44 - STALL_THRESHOLD_CHUNKS = 6 # Exit after this many chunks with no file growth 45 44 46 45 47 46 class MacOSObserver: ··· 68 67 # Multi-display tracking (similar to Linux observer) 69 68 self.current_displays: list[DisplayInfo] = [] 70 69 self.current_audio: AudioInfo | None = None 71 - self.last_video_sizes: dict[str, int] = {} 72 70 73 71 # Draft folder for current segment (HHMMSS_draft/) 74 72 self.draft_dir: str | None = None ··· 82 80 83 81 # Mute state at segment start 84 82 self.segment_is_muted = False 85 - 86 - # Health tracking 87 - self.files_growing = False 88 - self.stalled_chunks = ( 89 - 0 # Consecutive chunks with no file growth while capturing 90 - ) 91 83 92 84 async def setup(self): 93 85 """Initialize ScreenCaptureKit and Callosum connection.""" ··· 281 273 # Clear state 282 274 self.current_displays = [] 283 275 self.current_audio = None 284 - self.last_video_sizes = {} 285 - self.stalled_chunks = 0 286 276 287 277 # Rename draft folder to final segment name (atomic handoff) 288 278 if self.draft_dir and saved_files: ··· 377 367 self.current_displays = displays 378 368 self.current_audio = audio 379 369 self.capture_running = True 380 - self.last_video_sizes = {d.file_path: 0 for d in displays} 381 - self.stalled_chunks = 0 382 370 383 371 logger.info(f"Started capture with {len(displays)} display(s)") 384 372 for display in displays: ··· 434 422 "recording": True, 435 423 "streams": streams_info, 436 424 "window_elapsed_seconds": elapsed, 437 - "files_growing": self.files_growing, 438 425 } 439 426 else: 440 - screencast_info = {"recording": False, "files_growing": False} 427 + screencast_info = {"recording": False} 441 428 442 429 # Tmux info (not supported on macOS) 443 430 tmux_info = {"capturing": False} ··· 521 508 f"mute_change={mute_transition}" 522 509 ) 523 510 self.handle_boundary(is_active) 524 - 525 - # Check if capture files are actively growing (health indicator) 526 - if self.capture_running and self.current_displays: 527 - any_growing = False 528 - for display in self.current_displays: 529 - if os.path.exists(display.file_path): 530 - current_size = os.path.getsize(display.file_path) 531 - last_size = self.last_video_sizes.get(display.file_path, 0) 532 - if current_size > last_size: 533 - any_growing = True 534 - self.last_video_sizes[display.file_path] = current_size 535 - self.files_growing = any_growing 536 - 537 - # Fail-fast: exit if capture stalled (files not growing) 538 - if any_growing: 539 - self.stalled_chunks = 0 540 - else: 541 - self.stalled_chunks += 1 542 - if self.stalled_chunks >= STALL_THRESHOLD_CHUNKS: 543 - logger.error( 544 - f"Capture stalled for {self.stalled_chunks} chunks " 545 - f"({self.stalled_chunks * CHUNK_DURATION}s), exiting" 546 - ) 547 - self.running = False 548 - else: 549 - self.files_growing = False 550 - self.stalled_chunks = 0 551 511 552 512 # Emit status event 553 513 self.emit_status()