AGENTS.md#
Development guidelines for solstone-linux, a standalone Linux desktop observer.
Project Overview#
solstone-linux is a companion app that runs alongside the main solstone 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.
This 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.
Source Layout#
src/solstone_linux/
__init__.py Package version
cli.py CLI entry point (run, setup, install-service, status)
solstone-linux.service.in Systemd unit template (rendered by install-service)
config.py Config loading/persistence (~/.local/share/solstone-linux/)
observer.py Main capture loop — state machine (idle/screencast), audio + video
screencast.py Portal-based multi-monitor recording (xdg-desktop-portal + GStreamer)
audio_recorder.py Stereo audio recording (mic + system via soundcard)
audio_detect.py Audio device detection via ultrasonic tone
audio_mute.py PulseAudio mute state detection
activity.py Cross-desktop activity detection (screen lock, power save) via DBus
monitor_positions.py Monitor position assignment from geometry
session_env.py Desktop session environment checks and recovery
streams.py Stream name derivation (hostname-based)
sync.py Background sync service — uploads completed segments to server
upload.py HTTP upload client for solstone ingest server
recovery.py Crash recovery for orphaned .incomplete segments
tests/ pytest test suite
contrib/ Reference icons for development fallback
Architecture#
The observer runs a single asyncio event loop with two concurrent concerns:
-
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.incompleteand are renamed on finalization. -
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.
State 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.
The capture loop never makes network calls. It writes locally; sync handles all uploads.
Commands#
make install # Create venv, install package + dev tools (pytest, ruff) via uv
make test # Run all tests
make test-only TEST=tests/test_config.py # Run specific test
make format # Auto-format with ruff
make ci # Lint + format check + tests
make install-service # Smart install-or-upgrade: guards against cross-repo contamination; runs CI in upgrade mode
make service-restart # systemctl restart wrapper
make service-status # systemctl status wrapper
make service-logs # systemctl log tail wrapper
make uninstall-service # Disable + remove unit + pipx uninstall
make clean # Remove build artifacts and caches
make versions # Show installed package versions
Development Principles#
- Simple code. Prefer plain functions over classes. Use dataclasses for structured data. Only use classes when managing stateful lifecycle (Observer, Screencaster, SyncService, AudioRecorder).
- 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.
- 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.
- Atomic directory operations. Segments start as
HHMMSS.incomplete/, are renamed toHHMMSS_DDD/on completion, orHHMMSS.failed/on recovery failure. - System site-packages required. PyGObject and GStreamer bindings come from system packages. The venv (and pipx) must use
--system-site-packages.
File Headers#
All .py source files must include this header as the first two lines:
# SPDX-License-Identifier: AGPL-3.0-only
# Copyright (c) 2026 sol pbc
Add this header to new .py files in src/solstone_linux/ and tests/. Do not add headers to markdown, TOML, or config files.
Runtime Dependencies#
System packages (not pip-installable):
python3-gobject/python3-gi— PyGObject for GTK4 and GDK- GStreamer with PipeWire plugin (
gst-launch-1.0 pipewiresrc) - PipeWire running
pactl(PulseAudio utils) for mute detection- xdg-desktop-portal with ScreenCast support
Python packages (in pyproject.toml):
requests— HTTP upload clientnumpy— Audio buffer manipulation and RMS computationsoundfile— FLAC encodingsoundcard— Audio device enumeration and recordingdbus-next— Async DBus client for portal and activity detectionPyGObject— GDK monitor geometry (installed from system)
Data Paths#
- Config:
~/.local/share/solstone-linux/config/config.json - Captures:
~/.local/share/solstone-linux/captures/ - State:
~/.local/share/solstone-linux/state/ - Restore token:
~/.local/share/solstone-linux/config/restore_token - Install source marker:
~/.config/solstone-linux/.install-source(tracks which repo clone owns the pipx install)
Key Patterns#
- 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.
- Audio is stereo-interleaved. Left channel = microphone, right channel = system audio. When muted, channels are split into separate mono FLAC files.
- 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. - Crash recovery runs on startup.
recovery.pyscans for orphaned.incompletedirectories older than 2 minutes and finalizes or marks them as failed.
Testing#
Tests 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.
Brand canon#
- solstone-linux is an observer. In the system anatomy,
solstone = observers + sol agent + journal. This repo implements one of those observers. - 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.
- 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.
- 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. - 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.
Canon source of truth: sol pbc's internal brand canon (system-anatomy guide).
License#
AGPL-3.0-only -- Copyright (c) 2026 sol pbc