Review fixes: contact key race, history hot paths, build snapshots, test and doc cleanup - #175
Merged
Merged
Conversation
A second daemon can be constructed while the first is finishing a power cycle. It read the recovery journal at construction and, when it then failed to claim the bus name, still ran finish_shutdown() and could replay or clear restoration belonging to the live owner. Reload the journal only after claiming the name, and skip restoration during shutdown when the name was never acquired.
The Arch, Debian, and RPM builders packed every untracked, non-ignored file, so scratch directories, agent state, lock files, and a website checkout silently shipped in source archives. Snapshot the working-tree contents of indexed files, which still includes uncommitted edits, and list any untracked paths that were left out. Stage a new file with git add to include it.
BackendClient called GetStatus before every operation, doubling D-Bus round trips. Remember verified daemons by their unique bus name; a bus never reuses one, so a replacement daemon is still checked before its first call. The GTK client shares one cache across its per-call private connections.
Every ten history writes, prune_events decrypted the whole archive and rewrote every row's metadata on the GLib loop, and each MAP read event decrypted the whole table to find one handle. - Enforce the count and size ceilings on the hot path without decryption, and run the age sweep at most hourly (plus at every startup and unlock, as before). - Only normalize rows whose metadata actually needs it. - Index received-message handles incrementally. AUTOINCREMENT ids are never reused, so each lookup decrypts only rows appended since the last one; a random instance id in meta detects a recreated database.
The saved-target check calls GetManagedObjects every two seconds on the daemon's GLib loop with dbus-python's default 25 s timeout, so a wedged bluetoothd could stall every client request. Give it a 2 s timeout; a timeout reads as "cannot inspect", which the check already treats as transient.
The contact-sync state machine (MAP grace period, daily refresh, joined manual requests, storage-generation handling) lived in eight private Daemon fields. Tests reconstructed Daemon with __new__ and hand-set those fields, so they tracked the implementation and could assert behavior the real code never had. - Move it unchanged into ContactSync with an explicit public surface; Daemon wires it up and keeps only the setup-task and invalidation follow-up. The worker submit is looked up at call time. - Add isolated_state and make_daemon fixtures that build a real Daemon against temporary state with no D-Bus or Bluetooth I/O. - Test contact-sync behavior against ContactSync directly, and build real daemons in the lifecycle, pairing-policy, and storage-recovery tests. One pairing-policy assertion depended on a stub that skipped the manual-sync-satisfies-deferred-pull step; it now checks the real behavior.
The PBAP pull runs on the OBEX worker for up to minutes and encrypted with the live StorageSecurity. Relocking, a policy change, or fail_closed zeroes that key buffer in place on the GLib thread, so a concurrent encrypt could seal contact rows under a zeroed key; later reads then fail authentication and put storage into the error state. - Hand each pull its own snapshot of the key and release it afterwards. - Add StorageSecurity.revision, which changes with the key or policy. If it moved during a pull, discard the result: clear the cache, fail waiting callers with StorageChangedDuringSync (not reported as a Bluetooth failure), and download again once storage is writable.
Replace the last Daemon.__new__ constructions in the MAP-event, BlueZ setup, and private-bus Bluetooth setup tests with make_daemon, replacing only hardware-facing collaborators. Document the rule in TESTING.md.
The document had become one long run of prose mixing contracts with the details of individual bug fixes. Lead with a module map covering every backend, client, and Quickshell file, then group the invariants by area: process boundaries, D-Bus compatibility and roster tokens, clients, pairing, Bluetooth supervision, storage and privacy, lifecycle, tests. Replace the claim that every client shares onboarding and roster semantics with a Known duplication section: Quickshell re-implements stage derivation, and the shared ConversationLogic.qml re-implements roster logic in JavaScript.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A batch of fixes from a whole-project review. Each commit stands alone, and each one passes the full hermetic suite (
dbus-run-session+ pytest), Ruff, and mypy when checked out by itself. Reviewing commit by commit is easiest.Bluetooth recovery
26e0641). A second daemon, such asblueferry runwhile the service is running, read the recovery journal before claiming the name. If the claim failed, itsstop()still ranfinish_shutdown(), which could replay or clear restoration belonging to the live daemon. The journal is now reloaded after the claim, and restoration is skipped when the name was never acquired.Security
StorageSecurity. Relocking, a policy change, orfail_closedzeroes that key buffer in place on the GLib thread. A concurrentencryptcould therefore seal contact rows under a zeroed key, and later reads would fail authentication and put storage into the error state.StorageSecurity.revisionchanges whenever the key or policy does. If it changes during a pull, the result is discarded, the cache cleared, and the download re-queued.Daemon responsiveness
prune_eventsran on the GLib loop every 10 writes. It decrypted the whole archive (up to 10k rows) and UPDATEd every row, even rows already normalized. Withsecure_deleteon, that rewrote nearly the whole file every few incoming messages.GetManagedObjectsruns every 2s on the GLib loop with the default 25s timeout. It now uses a 2s timeout.BackendClientcalledGetStatusbefore every operation. Verified daemons are now cached by their unique bus name, which a replacement daemon never reuses.Packaging
build.shand the deb and rpm builders packed every untracked, non-ignored file, including.agents/,.codex/,uv.lock, and awebsite/checkout. Uncommitted edits to indexed files are still included, and untracked paths are listed.Tests and structure
Daemon. The state machine lived in eight privateDaemonfields and now lives inContactSync, moved without behavior changes.isolated_state/make_daemonfixtures construct a realDaemonagainst temporary state with no D-Bus or Bluetooth I/O. All 11Daemon.__new__+ hand-set-private-field constructions in tests are gone.Not in this PR
Validation