commits
Replace internal `~/projects/extro/cmo/brand/...` paths with neutral
phrasing referring to sol pbc's internal brand canon. The substantive
guidance (canon-permitted code vocabulary vs co-experience language for
owner-facing prose) is preserved.
Replace surveillance-flavored verbs in owner-facing prose ("captures
screen and audio", "screen and audio capture works", "capture source")
with the system-anatomy canon's co-experience phrasing
("experiences your screen and audio along with you"). solstone-linux
is now framed in AGENTS.md project-overview prose as one of the
trinity's observers.
Add a "Brand canon" section to AGENTS.md pointing to
~/projects/extro/cmo/brand/system-anatomy.md, naming the banned
verbs in branded copy, and clarifying that internal architecture
vocabulary (the Capture loop, capture pipeline, module names) is
canon-permitted in code-only contexts and stays as-is.
Internal capture-loop references in AGENTS.md (lines 19, 40, 46, 69)
are unchanged. CLAUDE.md remains a symlink to AGENTS.md and rides
automatically. No source, test, or config changes.
Mirrors the precedent sweeps in the parent solstone repo
(commits ddec60d5 and 929448de).
GNOME's org.freedesktop.ScreenSaver serves only the idle-inhibit
endpoints defined in the FDO idle-inhibit spec (Inhibit, UnInhibit,
SimulateUserActivity). GetActive is not part of the spec, so calling
it on GNOME raises DBusError("This method is not part of the idle
inhibition specification") — not ServiceUnknown/NameHasNoOwner. The
observable-probes rewrite in cbf581d made this surface as a WARN
every 5 s on GNOME Wayland hosts.
Detect GNOME at call time via case-insensitive token-equality match
on XDG_CURRENT_DESKTOP (colon-split, whitespace-stripped) and skip
the FDO branch entirely on GNOME. Non-GNOME desktops are unchanged
— FDO-first, GNOME fallback. The FDO block remains reachable on
KDE/XFCE/Cinnamon/MATE/etc.
Surfacing trail: lode 7ppx4a7g. Decision to go with
GNOME-as-early-skip (option b) recorded 2026-04-22.
Co-Authored-By: OpenAI Codex <codex@openai.com>
activity.py and screencast.py hid DBus probe failures behind broad
`except Exception: pass` and `return <literal>` fallbacks. Parser bugs,
bus-daemon dropouts, and timeouts all looked identical to "service not
present", which dropped operator signal and could silently flip
probe_activity_services() into always-capture mode when a real backend
was present but introspection broke.
Fix: inherit the doctor.check_portal pattern from 1629914. Service
presence probing now asks the bus daemon (org.freedesktop.DBus at
/org/freedesktop/DBus) whether a well-known name is owned via
NameHasOwner instead of introspecting each target service path. The
bus-daemon XML has no hyphenated members, so the parser bug does not
apply, and the code can distinguish "not registered" from "probe
broke".
This fixes seven sites: activity.probe_activity_services now uses
NameHasOwner against the bus daemon; activity.is_screen_locked narrows
both the FDO and GNOME fallback branches; activity.is_power_save_active
does the same for Mutter and KDE Solid; activity.get_monitor_geometries_kscreen
narrows its catch to concrete DBus/introspection failures; and
screencast.Screencaster._close_session now warns with the portal session
path instead of silently swallowing close failures.
One design choice is intentional: backend branches in the lock/power
fallback chains and KScreen suppress WARN logs when DBusError.type is
org.freedesktop.DBus.Error.ServiceUnknown or
org.freedesktop.DBus.Error.NameHasNoOwner. Without that, asymmetric
desktops such as vanilla GNOME (no KDE Solid) or KDE (no Mutter
DisplayConfig) would emit thousands of warnings per day from the 5s poll
loop. Any other probe failure stays loud: parser bugs, bus failures,
non-missing DBusError values, and close-session failures still warn.
_close_session and _name_has_owner log unconditionally because closing a
known session should work, and NameHasOwner returns False rather than
raising for truly missing services.
This closes the follow-up called out in the footer of 1629914, and adds
regression coverage for parser failures, broken backends, silent missing
services, and close-session logging.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
check_portal() previously introspected the portal object path directly,
which dbus-next refuses to parse on healthy GNOME sessions where the
portal exposes hyphenated property names such as power-saver-enabled.
The bare `except Exception` masked the real InvalidMemberNameError and
caused `solstone-linux doctor` to report `fail`, blocking
`make install-service` on working systems.
Fix: ask the D-Bus daemon (org.freedesktop.DBus) whether
org.freedesktop.portal.Desktop is a currently-owned well-known name via
NameHasOwner. The daemon's introspection XML has no hyphenated members,
so the parser bug does not apply. The check is bounded by a 2s
asyncio.wait_for and distinguishes three failure modes (not registered,
bus unreachable, timed out) with dedicated detail strings.
Adds five direct unit tests for check_portal, including a regression
pin that fails against the old implementation when a fake bus would
raise InvalidMemberNameError on portal-path introspection.
Follow-up (not in scope): activity.py and screencast.py also call
bus.introspect() on service object paths and would hit the same
parser bug on any service that exposes a hyphenated member; today
they hide it behind `except Exception: pass` and degrade silently.
Co-Authored-By: OpenAI Codex <codex@openai.com>
Modern SNI hosts such as the GNOME AppIndicator extension query IconAccessibleDesc and AttentionAccessibleDesc as optional properties. When they are absent, the host ends up logging missing-property warnings even though the rest of the indicator works.
Expose both properties on the StatusNotifierItem and update them from tray state so they track the current semantic status: recording, paused, idle, syncing, error, or stopped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GNOME AppIndicator treats LayoutUpdated as a structural menu change and tears down open submenus while they are animating. For the pause/resume and header label updates, property-only changes need to use ItemsPropertiesUpdated as described by the DBusMenu protocol.
Replace the old update_item path with update_properties, keep LayoutUpdated only for set_menu structural rebuilds, and switch AboutToShow to return False because GetLayout always reads fresh state and there are no pending unsignaled updates.
This supersedes the earlier noblink approach for these three tray call sites by sending precise property updates instead of forcing a structural refresh.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The zero-byte quarantine test hardcoded day 20260410 and asserted the
.failed dir survived cleanup. Once wall-clock time passed 2026-04-17
that day fell outside the 7-day retention window and cleanup correctly
deleted it, flipping the test red. Freeze sync.datetime.now() to keep
20260410 inside retention, and add a companion test in
TestCleanupFailedSegments covering the kept-within-retention case.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add an openSUSE zypper stanza verified on Tumbleweed 2026-04-22, a remote-sol subsection under "what to sort out together", rewrite the clone-location lead-in as a recommendation rather than a requirement, and add an AppIndicator fallback for distros without a packaged extension pinned to extensions.gnome.org v64 for GNOME Shell 45–50. Follow-up: README.md distro list still parallels INSTALL.md and will need the openSUSE recipe mirrored, but that is deferred and out of scope here.
Enforces the CLAUDE.md:71 contract ("The venv (and pipx) must use
--system-site-packages") for the dev venv. Previously `uv sync`
auto-created .venv without the flag and then built pycairo/pygobject
from source, so `make install-service` failed on any box lacking
cairo-devel / gobject-introspection-devel / glib2-devel / python3-devel.
Two-part fix in Makefile:
- Pre-create .venv with `uv venv --system-site-packages
--python /usr/bin/python3`. The interpreter pin is load-bearing:
uv's default fetches its own managed CPython, which defeats
--system-site-packages because the distro's gi/cairo live in the
system python's site-packages (e.g. /usr/lib64/python3.14 on
Fedora 43).
- Pass `--no-install-package pygobject --no-install-package pycairo`
to `uv sync` so the lock entries stay but they are not built into
the venv; the system packages are used instead.
`make clean` now also removes .venv. Existing dev boxes rebuild with
`make clean && make install`.
Design notes:
- Existence guard (`[ -f .venv/pyvenv.cfg ] ||`) instead of --clear,
keeping .installed idempotent. Wrong-state venvs require
`make clean` once; auto-detection wasn't worth the complexity.
- uv.lock left untouched: pycairo/pygobject stay resolved but are
skipped at install time (review doc L2 decision).
- pyproject.toml keeps PyGObject in [project.dependencies]
unchanged; pipx (which runs system python3 via
--system-site-packages) still needs the metadata declared.
Out-of-scope follow-up:
tests/test_sync.py::TestQuarantineZeroByte::test_zero_byte_segment_quarantined
fails on main today and continues to fail after this change. It
hardcodes date 20260410 and relies on default 7-day retention; on
2026-04-22 the .failed directory is swept by the cleanup pass in
the same _sync() call before the assertion runs. Reproducible on
both the old (CPython 3.13) and new (CPython 3.14) venv — not
caused by this lode.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Modern systemd (>= 230) requires StartLimitIntervalSec= and
StartLimitBurst= in [Unit]. In [Service] they are silently ignored,
so the observer's restart rate-limit never took effect and an
observer crash loop was not throttled. Reference: systemd.unit(5).
This eliminates the "Unknown key 'StartLimitIntervalSec'" and
"Unknown key 'StartLimitBurst'" warnings emitted by systemd when
loading the unit.
Existing installs pick up the fix on their next `make install-service`
run — no migration logic required.
Out of scope / follow-ups:
- solstone-tmux's unit template has the identical bug (separate lode).
- tests/test_sync.py::TestQuarantineZeroByte::test_zero_byte_segment_quarantined
fails on base as well; unrelated pre-existing mismatch between sync
quarantine cleanup and its test expectations.
Co-Authored-By: OpenAI Codex <codex@openai.com>
Single install-service target detects fresh-install vs upgrade via a
~/.config/solstone-linux/.install-source marker and runs CI in upgrade mode.
Marker ownership guard hard-fails on cross-repo contamination and on
pre-hygiene installs with no marker; manual remediation is the documented
recovery path. Drop the --force flag and Unit-unchanged short-circuit from
cmd_install_service — the unit is content-equivalent across clones so
unconditional rewrite is the simpler model.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a macOS-style plain-text status header at the top of the tray
menu. One pure helper `_compute_header_label(status, sync_status,
pause_remaining)` drives both the new header and the existing
`_status_item` inside the status submenu, so the two never diverge.
Uses em dash (U+2014) matching existing user-facing strings in
this file.
Fixes a post-pause resume-visibility bug: `MenuItem.get_properties()`
now always emits `enabled` and `visible` as boolean Variants rather
than only when False. Some SNI hosts (e.g. GNOME AppIndicator)
cache these values and do not re-default to True when the key is
absent, so after pause→resume the resume item would stay hidden
despite `LayoutUpdated`. Always emitting makes the spec-correct
boolean state visible on every layout re-read.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a packaged unit template and render it from install-service with
force/no-op behavior plus refreshed CLI coverage.
Add deploy/upgrade Make targets and collapse the install docs onto make deploy.
The in-process SNI tray was frozen once the observer entered a
paused state: the two call sites that previously drove tray
refreshes (post-boundary mode change and end-of-normal-iteration)
are unreachable from the paused branch, so the icon, status
submenu, and pause/resume menu items never reflected the new state.
Add a small Observer._refresh_tray() helper that wraps the
existing try/except + disable pattern, and invoke it from three
new points:
- Observer.pause() — immediate icon/menu flip when a pause is
initiated from the tray.
- Observer.resume() — immediate flip back on manual resume.
- Paused branch of main_loop() — runs once per 5s tick so the
"resume (N minutes remaining)" label counts down live and any
sync-status change during pause is reflected.
The three existing tray-update sites migrate to the same helper
for consistency (no behavior change at those sites).
Not manually smoke-tested on a GNOME Wayland session in this
change — coverage relies on the new pause/resume unit tests and
code review.
Co-Authored-By: Codex <noreply@openai.com>
install-service now writes Environment=PATH=... into the generated
unit file, with the venv bin directory prepended and duplicates removed.
Falls back to /usr/local/bin:/usr/bin:/bin when PATH is absent or empty.
Zero-byte segments (from GStreamer crashes) are detected before upload
and renamed to .failed. HTTP 400 (CLIENT) errors trigger quarantine
without incrementing the circuit breaker failure counter. Quarantined
.failed dirs are cleaned up on the same retention schedule as synced
segments.
Fixes a 3+ day infinite error loop where a single corrupt segment
would trip the circuit breaker, recover via probe, and immediately
re-fail.
Default 7 days. Values: positive int = days to keep, 0 = delete
immediately after confirmed sync, -1 = keep forever. Cleanup runs
at the end of each sync pass with triple-gated safety: synced_days
membership, age check, and per-segment server confirmation.
"copy help agent instructions" now lives under about — points
the user's coding agent to the installed source repo and INSTALL.md
for troubleshooting and configuration help.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The tray shouldn't kill the observer — it's a systemd service.
Quit item replaced with a grayed-out hint showing how the service
is managed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Strip HTML from tooltips — use plain text for KDE/GNOME compatibility
- INSTALL.md: add GNOME AppIndicator extension instructions (step 6)
with distro-specific install commands and verification steps
- Move "open journal" to top level above settings/about submenus
- Update activity detection note (works on KDE too now)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove update_item() calls for label-only updates in _update_status(),
_update_sync(), and _update_live_stats(). Labels are still assigned to
MenuItem objects so KDE reads fresh values via GetLayout on menu open.
Only structural changes (pause/resume visibility toggles) now emit
LayoutUpdated, fixing the 5-second menu blink on KDE.
Add a dedicated _start_mono timestamp that is initialized once when the observer starts and use it for uptime reporting in the tray and D-Bus stats service instead of start_at_mono, which resets on each segment boundary.
Also guard every update_item() call in TrayApp._update_live_stats() behind label-change checks so unchanged live stats do not emit redundant D-Bus LayoutUpdated signals and cause the tray menu to blink every 5 seconds.
Update the tray and D-Bus tests to seed _start_mono on mock observers and add a regression test covering the no-op live stats update path.
Replace broken GetStats() D-Bus call with direct filesystem stats computation (60s throttle)
Delete "show captures" menu item and _open_captures() method
Move "open journal" into settings submenu; reorder menu to: status > pause/resume > settings > about > quit
Lowercase all user-facing labels, tooltips, and submenu headers.
Rename active state from "recording" to "observing". Reorder menu
to match macOS (journal/captures before pause). Expand About submenu
with version, source code link, and PBC tagline. Update SVG icon
title and aria-label metadata across both icon directories.
The tray was a separate process with its own bus, polling loop, and
reconnection logic. Now it's an in-process component that shares the
observer's MessageBus and reads state directly. Pause/resume go through
Observer.pause()/resume() instead of D-Bus RPC. On headless systems the
tray silently skips. Removes solstone-tray entry point, install-tray CLI
command, and solstone-tray.desktop autostart file. Merges icon install
into install-service.
Port the system tray prototype into solstone-linux as a StatusNotifierItem app that talks to the Observer1 D-Bus interface.
Add the SNI, dbusmenu, and tray modules, installable SVG tray icons, a solstone-tray entry point, and tray-focused tests covering menu and status behavior.
- New dbus_service.py: ObserverService (ServiceInterface) exposing org.solpbc.solstone.Observer1 with 10 read-only properties, 3 methods (Pause, Resume, GetStats), and 3 signals (StatusChanged, SyncProgressChanged, ErrorOccurred)
- Observer gains pause/resume state (_paused, _pause_until) with clean segment finalization on pause and auto-resume on timer expiry
- SyncService tracks sync_status/sync_progress and emits SyncProgressChanged signal at key state transitions
- D-Bus service exported on the existing session bus connection in Observer.setup()
- 103 tests pass including 26 new tests for D-Bus properties, pause/resume, auto-resume, stats, sync status tracking
A 19-hour server outage revealed that once the circuit breaker tripped on
transient errors (5xx/network), sync was permanently disabled until service
restart. Add cooldown-based half-open state with exponential backoff
(30s → 60s → 120s → 300s cap) that probes the server via get_server_segments
before resuming full sync. Auth/revoked circuit stays permanently open.
On KDE Plasma, GDK is typically unavailable so screencast segments
would fall through to generic monitor-N labels. Add KScreen DBus
backend (org.kde.kscreen.Backend) as a fallback for monitor connector
names and positions. Add size-based stream matching for portals that
report (0,0) positions.
# Conflicts:
# src/solstone_linux/activity.py
# tests/test_activity.py
Remove get_idle_time_ms() and IDLE_THRESHOLD_MS entirely. Rewrite
is_screen_locked() with FDO ScreenSaver → GNOME ScreenSaver fallback
chain. Rewrite is_power_save_active() with GNOME Mutter → KDE Solid
fallback chain. Simplify observer mode logic to screen_locked or
power_save. Add XDG_CURRENT_DESKTOP to systemd PassEnvironment. Add
pytest-asyncio dev dependency and tests for all fallback paths.
Drop idle time polling and remove the GNOME idle monitor from activity detection, keeping observer mode selection based on screen lock or power save state.\n\nAdd freedesktop.ScreenSaver with GNOME ScreenSaver fallback for cross-desktop lock detection, and add KDE Solid PowerManagement as a power save fallback.\n\nUpdate both systemd unit definitions to pass XDG_CURRENT_DESKTOP through the user service environment, and add async fallback-chain tests with pytest-asyncio coverage.
Make activity detection crash-proof so the observer runs on any desktop:
- Wrap get_idle_time_ms() in try/except (was the sole crash cause on KDE)
- Add probe_activity_services() startup diagnostic — logs which DBus
services are available vs missing
- Protect GTK4 imports — module loads without GTK4, monitor geometry
detection degrades gracefully
- Wrap check_activity_status() calls in main loop — transient DBus
errors don't crash the observer
On non-GNOME desktops, observer logs "Activity signals unavailable"
and runs in always-capture mode. Includes ruff formatting fixes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Makefile provides install (uv + dev tools), test, format, ci, and clean
targets. AGENTS.md documents project architecture, source layout, commands,
conventions, and file header requirements. CLAUDE.md symlinks to AGENTS.md.
Also adds pytest and ruff as dev dependencies in pyproject.toml and
extends .gitignore for venv/uv artifacts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
API endpoints, CLI references, and stream_name parameter updated.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Instructions written for a coding agent working with a human.
Includes idempotency check, inline distro package lists,
sol root/observers clone path, and stream name as a
collaborative decision point.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous LICENSE contained only the short FSF boilerplate with
"either version 3 of the License, or (at your option) any later version"
— AGPL-3.0-or-later language that contradicted AGPL-3.0-only declared
in README, pyproject.toml, and SPDX headers in every source file.
Replace with the complete canonical AGPL-3.0 license text from the FSF.
The SPDX-License-Identifier headers in source files remain the
authoritative "only" declaration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When sol is installed on the same machine, use `sol remote --json create`
for registration instead of requiring the server to be running. Falls back
to HTTP registration if sol is not found or CLI fails.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Packages aren't on PyPI — document clone + pipx install from source.
Add Arch section with all required packages. Add gstreamer1-plugin-
pipewire to Fedora packages. Add note that activity detection (idle,
lock, power save) requires GNOME desktop — other desktops get capture
but no activity-based segment boundaries.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extracts the Linux desktop observer (screen + audio capture) from the
solstone monorepo into a standalone package. Follows the same pattern
as solstone-tmux (phase 5a) with key improvements:
- Local cache + async sync instead of inline upload at segment boundary
- Circuit breaker tuned by error type (auth=immediate, transient=5-10)
- Respects configured sync_max_retries (no hard min(config,3) cap)
- Recovery .metadata file with start timestamp for accurate duration
- Synced-days pruning at 90 days
- Session env recovery for CLI launch, PassEnvironment for systemd
- Screencast restore token at ~/.local/share/solstone-linux/config/
48 tests passing covering config, streams, monitor positions, recovery,
sync collection, error classification, circuit breaker thresholds, and
session readiness checks.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace surveillance-flavored verbs in owner-facing prose ("captures
screen and audio", "screen and audio capture works", "capture source")
with the system-anatomy canon's co-experience phrasing
("experiences your screen and audio along with you"). solstone-linux
is now framed in AGENTS.md project-overview prose as one of the
trinity's observers.
Add a "Brand canon" section to AGENTS.md pointing to
~/projects/extro/cmo/brand/system-anatomy.md, naming the banned
verbs in branded copy, and clarifying that internal architecture
vocabulary (the Capture loop, capture pipeline, module names) is
canon-permitted in code-only contexts and stays as-is.
Internal capture-loop references in AGENTS.md (lines 19, 40, 46, 69)
are unchanged. CLAUDE.md remains a symlink to AGENTS.md and rides
automatically. No source, test, or config changes.
Mirrors the precedent sweeps in the parent solstone repo
(commits ddec60d5 and 929448de).
GNOME's org.freedesktop.ScreenSaver serves only the idle-inhibit
endpoints defined in the FDO idle-inhibit spec (Inhibit, UnInhibit,
SimulateUserActivity). GetActive is not part of the spec, so calling
it on GNOME raises DBusError("This method is not part of the idle
inhibition specification") — not ServiceUnknown/NameHasNoOwner. The
observable-probes rewrite in cbf581d made this surface as a WARN
every 5 s on GNOME Wayland hosts.
Detect GNOME at call time via case-insensitive token-equality match
on XDG_CURRENT_DESKTOP (colon-split, whitespace-stripped) and skip
the FDO branch entirely on GNOME. Non-GNOME desktops are unchanged
— FDO-first, GNOME fallback. The FDO block remains reachable on
KDE/XFCE/Cinnamon/MATE/etc.
Surfacing trail: lode 7ppx4a7g. Decision to go with
GNOME-as-early-skip (option b) recorded 2026-04-22.
Co-Authored-By: OpenAI Codex <codex@openai.com>
activity.py and screencast.py hid DBus probe failures behind broad
`except Exception: pass` and `return <literal>` fallbacks. Parser bugs,
bus-daemon dropouts, and timeouts all looked identical to "service not
present", which dropped operator signal and could silently flip
probe_activity_services() into always-capture mode when a real backend
was present but introspection broke.
Fix: inherit the doctor.check_portal pattern from 1629914. Service
presence probing now asks the bus daemon (org.freedesktop.DBus at
/org/freedesktop/DBus) whether a well-known name is owned via
NameHasOwner instead of introspecting each target service path. The
bus-daemon XML has no hyphenated members, so the parser bug does not
apply, and the code can distinguish "not registered" from "probe
broke".
This fixes seven sites: activity.probe_activity_services now uses
NameHasOwner against the bus daemon; activity.is_screen_locked narrows
both the FDO and GNOME fallback branches; activity.is_power_save_active
does the same for Mutter and KDE Solid; activity.get_monitor_geometries_kscreen
narrows its catch to concrete DBus/introspection failures; and
screencast.Screencaster._close_session now warns with the portal session
path instead of silently swallowing close failures.
One design choice is intentional: backend branches in the lock/power
fallback chains and KScreen suppress WARN logs when DBusError.type is
org.freedesktop.DBus.Error.ServiceUnknown or
org.freedesktop.DBus.Error.NameHasNoOwner. Without that, asymmetric
desktops such as vanilla GNOME (no KDE Solid) or KDE (no Mutter
DisplayConfig) would emit thousands of warnings per day from the 5s poll
loop. Any other probe failure stays loud: parser bugs, bus failures,
non-missing DBusError values, and close-session failures still warn.
_close_session and _name_has_owner log unconditionally because closing a
known session should work, and NameHasOwner returns False rather than
raising for truly missing services.
This closes the follow-up called out in the footer of 1629914, and adds
regression coverage for parser failures, broken backends, silent missing
services, and close-session logging.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
check_portal() previously introspected the portal object path directly,
which dbus-next refuses to parse on healthy GNOME sessions where the
portal exposes hyphenated property names such as power-saver-enabled.
The bare `except Exception` masked the real InvalidMemberNameError and
caused `solstone-linux doctor` to report `fail`, blocking
`make install-service` on working systems.
Fix: ask the D-Bus daemon (org.freedesktop.DBus) whether
org.freedesktop.portal.Desktop is a currently-owned well-known name via
NameHasOwner. The daemon's introspection XML has no hyphenated members,
so the parser bug does not apply. The check is bounded by a 2s
asyncio.wait_for and distinguishes three failure modes (not registered,
bus unreachable, timed out) with dedicated detail strings.
Adds five direct unit tests for check_portal, including a regression
pin that fails against the old implementation when a fake bus would
raise InvalidMemberNameError on portal-path introspection.
Follow-up (not in scope): activity.py and screencast.py also call
bus.introspect() on service object paths and would hit the same
parser bug on any service that exposes a hyphenated member; today
they hide it behind `except Exception: pass` and degrade silently.
Co-Authored-By: OpenAI Codex <codex@openai.com>
Modern SNI hosts such as the GNOME AppIndicator extension query IconAccessibleDesc and AttentionAccessibleDesc as optional properties. When they are absent, the host ends up logging missing-property warnings even though the rest of the indicator works.
Expose both properties on the StatusNotifierItem and update them from tray state so they track the current semantic status: recording, paused, idle, syncing, error, or stopped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GNOME AppIndicator treats LayoutUpdated as a structural menu change and tears down open submenus while they are animating. For the pause/resume and header label updates, property-only changes need to use ItemsPropertiesUpdated as described by the DBusMenu protocol.
Replace the old update_item path with update_properties, keep LayoutUpdated only for set_menu structural rebuilds, and switch AboutToShow to return False because GetLayout always reads fresh state and there are no pending unsignaled updates.
This supersedes the earlier noblink approach for these three tray call sites by sending precise property updates instead of forcing a structural refresh.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The zero-byte quarantine test hardcoded day 20260410 and asserted the
.failed dir survived cleanup. Once wall-clock time passed 2026-04-17
that day fell outside the 7-day retention window and cleanup correctly
deleted it, flipping the test red. Freeze sync.datetime.now() to keep
20260410 inside retention, and add a companion test in
TestCleanupFailedSegments covering the kept-within-retention case.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add an openSUSE zypper stanza verified on Tumbleweed 2026-04-22, a remote-sol subsection under "what to sort out together", rewrite the clone-location lead-in as a recommendation rather than a requirement, and add an AppIndicator fallback for distros without a packaged extension pinned to extensions.gnome.org v64 for GNOME Shell 45–50. Follow-up: README.md distro list still parallels INSTALL.md and will need the openSUSE recipe mirrored, but that is deferred and out of scope here.
Enforces the CLAUDE.md:71 contract ("The venv (and pipx) must use
--system-site-packages") for the dev venv. Previously `uv sync`
auto-created .venv without the flag and then built pycairo/pygobject
from source, so `make install-service` failed on any box lacking
cairo-devel / gobject-introspection-devel / glib2-devel / python3-devel.
Two-part fix in Makefile:
- Pre-create .venv with `uv venv --system-site-packages
--python /usr/bin/python3`. The interpreter pin is load-bearing:
uv's default fetches its own managed CPython, which defeats
--system-site-packages because the distro's gi/cairo live in the
system python's site-packages (e.g. /usr/lib64/python3.14 on
Fedora 43).
- Pass `--no-install-package pygobject --no-install-package pycairo`
to `uv sync` so the lock entries stay but they are not built into
the venv; the system packages are used instead.
`make clean` now also removes .venv. Existing dev boxes rebuild with
`make clean && make install`.
Design notes:
- Existence guard (`[ -f .venv/pyvenv.cfg ] ||`) instead of --clear,
keeping .installed idempotent. Wrong-state venvs require
`make clean` once; auto-detection wasn't worth the complexity.
- uv.lock left untouched: pycairo/pygobject stay resolved but are
skipped at install time (review doc L2 decision).
- pyproject.toml keeps PyGObject in [project.dependencies]
unchanged; pipx (which runs system python3 via
--system-site-packages) still needs the metadata declared.
Out-of-scope follow-up:
tests/test_sync.py::TestQuarantineZeroByte::test_zero_byte_segment_quarantined
fails on main today and continues to fail after this change. It
hardcodes date 20260410 and relies on default 7-day retention; on
2026-04-22 the .failed directory is swept by the cleanup pass in
the same _sync() call before the assertion runs. Reproducible on
both the old (CPython 3.13) and new (CPython 3.14) venv — not
caused by this lode.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Modern systemd (>= 230) requires StartLimitIntervalSec= and
StartLimitBurst= in [Unit]. In [Service] they are silently ignored,
so the observer's restart rate-limit never took effect and an
observer crash loop was not throttled. Reference: systemd.unit(5).
This eliminates the "Unknown key 'StartLimitIntervalSec'" and
"Unknown key 'StartLimitBurst'" warnings emitted by systemd when
loading the unit.
Existing installs pick up the fix on their next `make install-service`
run — no migration logic required.
Out of scope / follow-ups:
- solstone-tmux's unit template has the identical bug (separate lode).
- tests/test_sync.py::TestQuarantineZeroByte::test_zero_byte_segment_quarantined
fails on base as well; unrelated pre-existing mismatch between sync
quarantine cleanup and its test expectations.
Co-Authored-By: OpenAI Codex <codex@openai.com>
Single install-service target detects fresh-install vs upgrade via a
~/.config/solstone-linux/.install-source marker and runs CI in upgrade mode.
Marker ownership guard hard-fails on cross-repo contamination and on
pre-hygiene installs with no marker; manual remediation is the documented
recovery path. Drop the --force flag and Unit-unchanged short-circuit from
cmd_install_service — the unit is content-equivalent across clones so
unconditional rewrite is the simpler model.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a macOS-style plain-text status header at the top of the tray
menu. One pure helper `_compute_header_label(status, sync_status,
pause_remaining)` drives both the new header and the existing
`_status_item` inside the status submenu, so the two never diverge.
Uses em dash (U+2014) matching existing user-facing strings in
this file.
Fixes a post-pause resume-visibility bug: `MenuItem.get_properties()`
now always emits `enabled` and `visible` as boolean Variants rather
than only when False. Some SNI hosts (e.g. GNOME AppIndicator)
cache these values and do not re-default to True when the key is
absent, so after pause→resume the resume item would stay hidden
despite `LayoutUpdated`. Always emitting makes the spec-correct
boolean state visible on every layout re-read.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The in-process SNI tray was frozen once the observer entered a
paused state: the two call sites that previously drove tray
refreshes (post-boundary mode change and end-of-normal-iteration)
are unreachable from the paused branch, so the icon, status
submenu, and pause/resume menu items never reflected the new state.
Add a small Observer._refresh_tray() helper that wraps the
existing try/except + disable pattern, and invoke it from three
new points:
- Observer.pause() — immediate icon/menu flip when a pause is
initiated from the tray.
- Observer.resume() — immediate flip back on manual resume.
- Paused branch of main_loop() — runs once per 5s tick so the
"resume (N minutes remaining)" label counts down live and any
sync-status change during pause is reflected.
The three existing tray-update sites migrate to the same helper
for consistency (no behavior change at those sites).
Not manually smoke-tested on a GNOME Wayland session in this
change — coverage relies on the new pause/resume unit tests and
code review.
Co-Authored-By: Codex <noreply@openai.com>
Zero-byte segments (from GStreamer crashes) are detected before upload
and renamed to .failed. HTTP 400 (CLIENT) errors trigger quarantine
without incrementing the circuit breaker failure counter. Quarantined
.failed dirs are cleaned up on the same retention schedule as synced
segments.
Fixes a 3+ day infinite error loop where a single corrupt segment
would trip the circuit breaker, recover via probe, and immediately
re-fail.
- Strip HTML from tooltips — use plain text for KDE/GNOME compatibility
- INSTALL.md: add GNOME AppIndicator extension instructions (step 6)
with distro-specific install commands and verification steps
- Move "open journal" to top level above settings/about submenus
- Update activity detection note (works on KDE too now)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove update_item() calls for label-only updates in _update_status(),
_update_sync(), and _update_live_stats(). Labels are still assigned to
MenuItem objects so KDE reads fresh values via GetLayout on menu open.
Only structural changes (pause/resume visibility toggles) now emit
LayoutUpdated, fixing the 5-second menu blink on KDE.
Add a dedicated _start_mono timestamp that is initialized once when the observer starts and use it for uptime reporting in the tray and D-Bus stats service instead of start_at_mono, which resets on each segment boundary.
Also guard every update_item() call in TrayApp._update_live_stats() behind label-change checks so unchanged live stats do not emit redundant D-Bus LayoutUpdated signals and cause the tray menu to blink every 5 seconds.
Update the tray and D-Bus tests to seed _start_mono on mock observers and add a regression test covering the no-op live stats update path.
Lowercase all user-facing labels, tooltips, and submenu headers.
Rename active state from "recording" to "observing". Reorder menu
to match macOS (journal/captures before pause). Expand About submenu
with version, source code link, and PBC tagline. Update SVG icon
title and aria-label metadata across both icon directories.
The tray was a separate process with its own bus, polling loop, and
reconnection logic. Now it's an in-process component that shares the
observer's MessageBus and reads state directly. Pause/resume go through
Observer.pause()/resume() instead of D-Bus RPC. On headless systems the
tray silently skips. Removes solstone-tray entry point, install-tray CLI
command, and solstone-tray.desktop autostart file. Merges icon install
into install-service.
- New dbus_service.py: ObserverService (ServiceInterface) exposing org.solpbc.solstone.Observer1 with 10 read-only properties, 3 methods (Pause, Resume, GetStats), and 3 signals (StatusChanged, SyncProgressChanged, ErrorOccurred)
- Observer gains pause/resume state (_paused, _pause_until) with clean segment finalization on pause and auto-resume on timer expiry
- SyncService tracks sync_status/sync_progress and emits SyncProgressChanged signal at key state transitions
- D-Bus service exported on the existing session bus connection in Observer.setup()
- 103 tests pass including 26 new tests for D-Bus properties, pause/resume, auto-resume, stats, sync status tracking
A 19-hour server outage revealed that once the circuit breaker tripped on
transient errors (5xx/network), sync was permanently disabled until service
restart. Add cooldown-based half-open state with exponential backoff
(30s → 60s → 120s → 300s cap) that probes the server via get_server_segments
before resuming full sync. Auth/revoked circuit stays permanently open.
Remove get_idle_time_ms() and IDLE_THRESHOLD_MS entirely. Rewrite
is_screen_locked() with FDO ScreenSaver → GNOME ScreenSaver fallback
chain. Rewrite is_power_save_active() with GNOME Mutter → KDE Solid
fallback chain. Simplify observer mode logic to screen_locked or
power_save. Add XDG_CURRENT_DESKTOP to systemd PassEnvironment. Add
pytest-asyncio dev dependency and tests for all fallback paths.
Drop idle time polling and remove the GNOME idle monitor from activity detection, keeping observer mode selection based on screen lock or power save state.\n\nAdd freedesktop.ScreenSaver with GNOME ScreenSaver fallback for cross-desktop lock detection, and add KDE Solid PowerManagement as a power save fallback.\n\nUpdate both systemd unit definitions to pass XDG_CURRENT_DESKTOP through the user service environment, and add async fallback-chain tests with pytest-asyncio coverage.
Make activity detection crash-proof so the observer runs on any desktop:
- Wrap get_idle_time_ms() in try/except (was the sole crash cause on KDE)
- Add probe_activity_services() startup diagnostic — logs which DBus
services are available vs missing
- Protect GTK4 imports — module loads without GTK4, monitor geometry
detection degrades gracefully
- Wrap check_activity_status() calls in main loop — transient DBus
errors don't crash the observer
On non-GNOME desktops, observer logs "Activity signals unavailable"
and runs in always-capture mode. Includes ruff formatting fixes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Makefile provides install (uv + dev tools), test, format, ci, and clean
targets. AGENTS.md documents project architecture, source layout, commands,
conventions, and file header requirements. CLAUDE.md symlinks to AGENTS.md.
Also adds pytest and ruff as dev dependencies in pyproject.toml and
extends .gitignore for venv/uv artifacts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous LICENSE contained only the short FSF boilerplate with
"either version 3 of the License, or (at your option) any later version"
— AGPL-3.0-or-later language that contradicted AGPL-3.0-only declared
in README, pyproject.toml, and SPDX headers in every source file.
Replace with the complete canonical AGPL-3.0 license text from the FSF.
The SPDX-License-Identifier headers in source files remain the
authoritative "only" declaration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Packages aren't on PyPI — document clone + pipx install from source.
Add Arch section with all required packages. Add gstreamer1-plugin-
pipewire to Fedora packages. Add note that activity detection (idle,
lock, power save) requires GNOME desktop — other desktops get capture
but no activity-based segment boundaries.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extracts the Linux desktop observer (screen + audio capture) from the
solstone monorepo into a standalone package. Follows the same pattern
as solstone-tmux (phase 5a) with key improvements:
- Local cache + async sync instead of inline upload at segment boundary
- Circuit breaker tuned by error type (auth=immediate, transient=5-10)
- Respects configured sync_max_retries (no hard min(config,3) cap)
- Recovery .metadata file with start timestamp for accurate duration
- Synced-days pruning at 90 days
- Session env recovery for CLI launch, PassEnvironment for systemd
- Screencast restore token at ~/.local/share/solstone-linux/config/
48 tests passing covering config, streams, monitor positions, recovery,
sync collection, error classification, circuit breaker thresholds, and
session readiness checks.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>