linux observer
1# AGENTS.md
2
3Development guidelines for solstone-linux, a standalone Linux desktop observer.
4
5## Project Overview
6
7solstone-linux is a companion app that runs alongside the main [solstone](https://solstone.app) journal. It is one of the owner's observers — it experiences screen and audio along with the owner on a Linux desktop using PipeWire and GStreamer, stores segments locally, and syncs them to a solstone server. It runs as a systemd user service on GNOME Wayland sessions.
8
9This is **not** part of the solstone monorepo. It is a standalone package with its own release lifecycle, installed via pipx alongside system-provided PyGObject/GStreamer bindings.
10
11## Source Layout
12
13```
14src/solstone_linux/
15 __init__.py Package version
16 cli.py CLI entry point (run, setup, install-service, status)
17 solstone-linux.service.in Systemd unit template (rendered by install-service)
18 config.py Config loading/persistence (~/.local/share/solstone-linux/)
19 observer.py Main capture loop — state machine (idle/screencast), audio + video
20 screencast.py Portal-based multi-monitor recording (xdg-desktop-portal + GStreamer)
21 audio_recorder.py Stereo audio recording (mic + system via soundcard)
22 audio_detect.py Audio device detection via ultrasonic tone
23 audio_mute.py PulseAudio mute state detection
24 activity.py Cross-desktop activity detection (screen lock, power save) via DBus
25 monitor_positions.py Monitor position assignment from geometry
26 session_env.py Desktop session environment checks and recovery
27 streams.py Stream name derivation (hostname-based)
28 sync.py Background sync service — uploads completed segments to server
29 upload.py HTTP upload client for solstone ingest server
30 recovery.py Crash recovery for orphaned .incomplete segments
31
32tests/ pytest test suite
33contrib/ Reference icons for development fallback
34```
35
36## Architecture
37
38The observer runs a single asyncio event loop with two concurrent concerns:
39
401. **Capture loop** (`observer.py`) — Checks activity status every 5 seconds, records audio continuously, manages screencast recording via GStreamer. Creates 5-minute segments in `~/.local/share/solstone-linux/captures/YYYYMMDD/stream/HHMMSS_DDD/`. Segment directories start as `.incomplete` and are renamed on finalization.
41
422. **Sync service** (`sync.py`) — Background asyncio task that walks the captures directory, queries the server for existing segments, and uploads missing ones. Circuit breaker pattern with error-type-aware thresholds.
43
44State machine has two modes: `screencast` (screen active, recording video) and `idle` (screen inactive). Mode transitions, mute state changes, and 5-minute intervals all trigger segment boundaries.
45
46The capture loop never makes network calls. It writes locally; sync handles all uploads.
47
48## Commands
49
50```bash
51make install # Create venv, install package + dev tools (pytest, ruff) via uv
52make test # Run all tests
53make test-only TEST=tests/test_config.py # Run specific test
54make format # Auto-format with ruff
55make ci # Lint + format check + tests
56make install-service # Smart install-or-upgrade: guards against cross-repo contamination; runs CI in upgrade mode
57make service-restart # systemctl restart wrapper
58make service-status # systemctl status wrapper
59make service-logs # systemctl log tail wrapper
60make uninstall-service # Disable + remove unit + pipx uninstall
61make clean # Remove build artifacts and caches
62make versions # Show installed package versions
63```
64
65## Development Principles
66
67- **Simple code.** Prefer plain functions over classes. Use dataclasses for structured data. Only use classes when managing stateful lifecycle (Observer, Screencaster, SyncService, AudioRecorder).
68- **Async by default.** The main loop is asyncio. DBus calls, subprocess management, and sync all use async. Audio recording uses a dedicated thread because soundcard is blocking.
69- **No network in the capture loop.** The observer writes segments locally. The sync service uploads asynchronously. This keeps capture reliable even when the server is down.
70- **Atomic directory operations.** Segments start as `HHMMSS.incomplete/`, are renamed to `HHMMSS_DDD/` on completion, or `HHMMSS.failed/` on recovery failure.
71- **System site-packages required.** PyGObject and GStreamer bindings come from system packages. The venv (and pipx) must use `--system-site-packages`.
72
73## File Headers
74
75All `.py` source files must include this header as the first two lines:
76
77```python
78# SPDX-License-Identifier: AGPL-3.0-only
79# Copyright (c) 2026 sol pbc
80```
81
82Add this header to new `.py` files in `src/solstone_linux/` and `tests/`. Do not add headers to markdown, TOML, or config files.
83
84## Runtime Dependencies
85
86System packages (not pip-installable):
87- `python3-gobject` / `python3-gi` — PyGObject for GTK4 and GDK
88- GStreamer with PipeWire plugin (`gst-launch-1.0 pipewiresrc`)
89- PipeWire running
90- `pactl` (PulseAudio utils) for mute detection
91- xdg-desktop-portal with ScreenCast support
92
93Python packages (in pyproject.toml):
94- `requests` — HTTP upload client
95- `numpy` — Audio buffer manipulation and RMS computation
96- `soundfile` — FLAC encoding
97- `soundcard` — Audio device enumeration and recording
98- `dbus-next` — Async DBus client for portal and activity detection
99- `PyGObject` — GDK monitor geometry (installed from system)
100
101## Data Paths
102
103- Config: `~/.local/share/solstone-linux/config/config.json`
104- Captures: `~/.local/share/solstone-linux/captures/`
105- State: `~/.local/share/solstone-linux/state/`
106- Restore token: `~/.local/share/solstone-linux/config/restore_token`
107- Install source marker: `~/.config/solstone-linux/.install-source` (tracks which repo clone owns the pipx install)
108
109## Key Patterns
110
111- **Activity detection is cross-desktop.** Uses ordered DBus fallback chains for screen lock (freedesktop.org ScreenSaver → GNOME ScreenSaver) and power save (Mutter DisplayConfig → KDE Solid PowerManagement). All backends degrade gracefully to safe defaults.
112- **Audio is stereo-interleaved.** Left channel = microphone, right channel = system audio. When muted, channels are split into separate mono FLAC files.
113- **Screencast uses xdg-desktop-portal.** Session persistence via restore tokens avoids re-prompting the user. GStreamer subprocess (`gst-launch-1.0`) handles the actual PipeWire recording.
114- **Crash recovery runs on startup.** `recovery.py` scans for orphaned `.incomplete` directories older than 2 minutes and finalizes or marks them as failed.
115
116## Testing
117
118Tests use pytest with standard mocking. No system dependencies required for tests — audio devices, DBus, and GStreamer are mocked. Run `make test` to execute the full suite.
119
120## Brand canon
121
122- **solstone-linux is an observer.** In the system anatomy, `solstone = observers + sol agent + journal`. This repo implements one of those observers.
123- **The canon lives elsewhere.** Owner-facing terminology comes from sol pbc's internal brand canon (system anatomy + voice terminology guides). This repo's branded prose follows it; the canon itself is not vendored here.
124- **Use co-experience language in branded prose.** In README, INSTALL, onboarding text, settings copy, and error messages, describe solstone-linux as something that experiences screen and audio along with the owner. Never describe it as watching, recording, monitoring, or tracking the owner.
125- **Keep code language in code-only contexts.** Internal architecture terms such as `Capture loop`, the capture pipeline, module names, and data-path names are canon-permitted here and must not be renamed just to match branded prose.
126- **Edit with the surface in mind.** If the owner sees the string, follow the canon. If the text is naming code, pipelines, modules, or storage artifacts for engineers, the existing internal vocabulary stays.
127
128Canon source of truth: sol pbc's internal brand canon (system-anatomy guide).
129
130## License
131
132AGPL-3.0-only -- Copyright (c) 2026 sol pbc