Unreleased

Changes on main since v0.8.89, one line each. The version in Cargo.toml is already 0.8.90, but nothing is tagged yet: these ship when the owner decides to release, and no date is promised here.

  • One interface on every device. The app is three sections — Home, Devices, Settings — on desktops and phones alike. Home is the old Overview and History as one page: the connection status said once as a plain line, then everything synced, newest first, one row per item. The Latest Synced / Latest File / Group Clipboard cards, the filter pills and the second online counter are gone.
  • A server is a computer. A device is a Phone, a Tablet or a Computer — one rule, three icons; the separate «Servers» group and the Server / Terminal / WSL labels are gone.
  • Copy is Copy: one act per item, and it copies the item itself. «Copy» on a picture puts the picture on the clipboard, «Copy file» puts the file; the path is a separate, secondary Path act. Each act is shown only where the daemon says this device’s clipboard can hold it.
  • Phones copy a picture as the picture, everywhere. On Android and iOS — in the app, in the arrival offer and on the notification — a picture lands on the clipboard as an image an app can paste, instead of a path that pasted as text and was useless there.
  • A group of phones only works, and says what it cannot do. With no computer in the group there is no Path act (a path pastes nowhere), a file is copied as a file, and a share counts as delivered when a device that can actually use it has the bytes.
  • The iPhone notification shows the picture. The notice carries a small end-to-end encrypted thumbnail, so the expanded card shows what arrived. Copy there puts the picture when its bytes are already on the phone; when they are not, the card waits briefly and then opens Clipshot, which applies the picture. It never puts a path on a phone.
  • «Waiting for <sender>». In a group with no computer, a phone that learns an item exists before its bytes arrive says so on Home — «Waiting for Pixel — open Clipshot there» — instead of showing nothing.
  • A long text can be copied straight from the iPhone notice: above the plain 2.6 KB limit a text of up to 64 KB travels compressed and still end-to-end encrypted, and the card copies it with the app closed.
  • macOS: Cmd+B survives another app’s secure keyboard entry. While an app holds Secure Event Input (a password field in Chrome, say), Clipshot registers the paste-path and sync-toggle combos the other way around and a one-line banner says who holds it and until when. Before, Cmd+B silently did nothing.
  • Pairing keeps the established group — proven against the live portal, both directions: a fresh device joins the existing group instead of pulling an established device into its own.
  • iOS: a suspended app is no longer reported as a stall. A tick stage that spanned a suspension is logged as exactly that, so a real stall while the app is awake stands out.
  • One received item is one row, and a picture sent as a file is shown as a picture, with a thumbnail and a working Copy.
  • A clipboard text that is just a group path is not re-sent as new content (the Universal Clipboard echo), and neither is a text that is the previous text re-encoded.
  • Activity ids stay distinct past the 100-event cap — after a day of use, History had been collapsing every received text into one row.
  • No white screen when returning to the app on Android, and the first frame is already the rendered app.

v0.8.89 — 2026-09-29

  • A device back from downtime gets what it is owed within seconds. Catch-up now reaches a device that outbound pushes had put in quarantine, and a delivered catch-up lifts that quarantine — before, an owed screenshot waited for the next unrelated copy (measured: six minutes).
  • Reconnect never dials a third party’s relay. Only the target’s own relay can reach it, so the step that borrowed another device’s relay (and credited it when iroh connected on its own) is gone.

v0.8.88 — 2026-09-28

  • The iPhone never hangs on «Connecting to the local daemon…» after a background. iOS reclaims a suspended app’s listening socket, which killed the local HTTP server silently; it is now rebound, and a probe on return to front heals a listener that died without an error.
  • The local API watches itself on every platform — it asks itself GET /health every 30 s and rebinds after two unanswered probes, so a silent listener death is caught on Android and desktops too, not only on an iOS return to front.
  • A dead daemon ends the process instead of answering from stale caches (iOS and Android), so the next open or push starts clean; Android’s service starts it again.
  • A share from the share sheet reaches devices that were away. The desktop that accepts an extension’s item now owes it to peers on their next reconnect, in the author’s name — before, a laptop asleep during an iPhone share never got the screenshot.
  • clipshot update on a GUI build fetches the GUI asset, and refuses a non-GUI one even if the manifest offers it — a Linux GUI service had been updating itself into a headless binary and then failing to start.
  • The macOS auto-update ships the Developer ID-signed app. Every update before this installed an ad-hoc-signed build, which quietly dropped the app’s privacy grants.

v0.8.87 — 2026-09-27

  • A share from the iOS share sheet now wakes the phones that are away. The Share Extension has no hub connection of its own, and only an item’s origin ever announced a new item — so a share straight from the sheet notified nobody (measured 2026-09-27: six iPhone shares, zero notices on the suspended iPad). Now the desktop that accepts the item announces it on the origin’s behalf when the item is new to it, and the notice names the device that made it («from iPhone»), not the one that relayed it. The origin itself is never woken about its own share.
  • A copy that happened says so. Applying an offer on a phone now shows a confirmation — «Path copied — image from iPhone» with the group path, «Text copied», «Image copied» on Android — and the offer bar hides itself once the item is on the pasteboard. Bringing Clipshot to the front has no tap of its own, so before this a successful copy and nothing at all looked exactly the same.
  • A duplicate delivery never resets a completed offer. When the same item (same kind, size, author and origin timestamp) arrives a second time — a catch-up re-offer during a reconnect burst, for example — the offer keeps the file it already has, instead of dropping back to «nothing was saved for this text» when you tap Copy.
  • The phone’s local bridge secret is one per install, not one per launch. A reader that had cached the previous launch’s value held a dead secret, and the app’s own page sat on «Connecting to the local daemon… (bridge auth required)» while the share itself landed fine. Regenerating it bought no security — the file is app-private either way.
  • Android: a cold start waits for the daemon. The WebView shows a local «Starting Clipshot…» page and loads the app the moment the daemon answers, instead of opening a URL nothing answers yet and rendering Chromium’s connection-refused error page.
  • Android release APKs are signed with the release key when a keystore is configured — they were debug-signed whatever the environment said.

v0.8.86 — 2026-09-27

  • iOS: a pasteboard write is confirmed by the system, not by our own readback. The Share Extension reads pasteboardd’s change counter: a readback that matches while the counter has not moved is a refused write, and the share sheet says so instead of reporting success.
  • A refused or empty pasteboard write is retried on a fixed 150 / 400 / 1000 ms schedule — never when something else wrote after us, which stays the other writer’s item.
  • One rule decides the outcome of a pasteboard write (written / refused / kept other / empty), and the share report carries how many attempts the group-path write needed.
  • The Share Extension’s diagnostics are drained after logging starts, so its lines actually reach the log — five cold launches had shown none of them.
  • iOS hardware testing runs over a Tailscale tunnel (no USB, no shared LAN): the owner’s screenshot → Share → paste flow measured 8/8.
  • Consistency work after v0.8.85: the notification card’s sync-name rule mirrors the daemon’s exactly, and file names stay identical across nodes.

v0.8.85 — 2026-09-21

  • The notification carries the item itself. A text of up to 2.6 KB travels end-to-end encrypted inside the push payload, and an image or file travels as its group path, so Copy on the expanded notification card works with Clipshot terminated — no app launch, no network, no wake (measured 10/10 on the simulator and on an iPhone 15).
  • File names are consistent across nodes: the sender’s YYYYMMDD_<name> sync name travels on the wire and receivers keep it, and minting it is idempotent so a forward or a retry never adds a second date prefix.
  • iOS notices are passive — a row in Notification Center for the moment you paste, with no banner, no sound, and no reliance on background delivery.
  • A Universal Clipboard item syncs but wakes no phone: content the Mac received from another Apple device of yours is still shared with the group, but the phone that copied it is no longer notified about its own clipboard.
  • Linux: a received item is no longer reverted by the clipboard manager — an X11 write hands the selection over to the new owner instead of leaving it ownerless for a moment (KDE’s Klipper used to re-serve the previous item over it).
  • A big share resumes after the receiver restarts: an iPad Share Extension continued a 45 MB item from the host’s offset mid-transfer, with identical checksums on both hosts.
  • iOS simulator lane: notification scenarios now skip with a stated reason when a live fleet item interferes, instead of failing falsely.

v0.8.84 — 2026-09-21

  • A device is never node_NNNN. iPhone and iPad now write their real device name (then the model) at startup, Android already wrote Build.MODEL, and the fallback for a device without a name reads unnamed-device-NNNN — the owner’s iPhone had been sitting in the fleet as node_7120 beside dead test nodes, and a cleanup pass removed it from the group.
  • Pairing keeps the group that already exists. A token is adopted only from a device that HAS a group: a joiner with an established group keeps it against a fresh generator’s provisional token, and the fresh generator joins the joiner’s group instead.
  • iOS: the expanded notification’s Copy button copies exactly the item the notice is about. The notice carries the item’s identity (kind, size, origin timestamp), so a card whose item has not landed yet reads «Arriving…» and waits for that item instead of copying a newer one. Before the first unlock after a reboot the card says «Unlock to copy» and copies nothing.
  • A group path is a reference, not content. A clipboard text that is exactly a ~/.clipshot/sync/<name> path — for example the one Apple Universal Clipboard carries from a phone to a Mac — is no longer broadcast as a new text item, so the group stops filling with .txt files whose body is a path.
  • Presence on iPhone/iPad is «resumed AND focused», and it is sent by the hub connection itself rather than queued on the daemon tick: a phone that leaves the foreground is no longer counted as present, so the hub sends it the notice it was missing. A visible-but-unfocused window (iPad Windowed Apps / Split View) counts as not present.
  • Peers: connection_type is the path the last health probe travelled (direct / relay), and is absent until a probe has reported one. It used to be read off the address card, so every hub-known peer showed relay while the node held direct QUIC connections; measured after the fix on main: 5 of 6 peers direct, the LTE phone relay.
  • The tick never dials a peer inline. A peer named by the hub or by gossip is registered and handed to the reconnect scheduler, which dials one peer per cycle under its own cap — one offline peer used to freeze the tick for ~30 s on every hub (re)connect, i.e. on every phone wake. Every tick stage is timed now, and a stage over 2 s logs a warning naming itself.
  • A removal the hub has already forgotten stops being re-sent. A device that an authoritative peer snapshot no longer lists is treated as confirmed removed, instead of re-sending RemoveDevice once a minute for up to 7 days.
  • One backoff owner for relay dials — connect_via_relay is a single dial, not a second retry loop beside the reconnect scheduler.
  • Cmd+B pastes the path it decided, instead of falling back to an older file from the sync directory.
  • Onboarding is one card, not a four-slide tour — three lines for the host you are on (desktop: copy here / Ctrl+B pastes the path / History; phone: Share → Clipshot / what arrives is offered, tap Copy / History) and it no longer covers the arrival-offer bar.

v0.8.83 — 2026-09-17

  • Phones: an arrival is an offer. Nothing that arrives on a phone or tablet writes the pasteboard by itself. It changes only by your own act: your own send, a tap on the offer («Image (1.2 MB) from main — Copy path» in the app, Copy / Copy path on the Android notification), or bringing Clipshot to the front — which applies the newest offer only if it arrived after the last thing you put there yourself. The 60 s “own send owns the pasteboard” window from v0.8.82 is replaced by this rule.
  • Offers survive an app relaunch — a screenshot that arrived while the app was closed is still one tap away after reopening.
  • A phone’s own clipboard is never mirrored; the only way in from a phone is Share → Clipshot. The mirror_mobile_clipboard setting is removed (it had no UI and decided something the platform decides).
  • A phone is not a host. A share from a phone counts as delivered when a desktop or server has it; the phone’s pasteboard gets the group path at that moment. If only another phone took the bytes, the share sheet says so and holds the item instead of reporting success.
  • Big items resume after a drop. From 8 MiB up an item travels in 1 MiB chunks; the receiver keeps a partial and any sender of the same item — the original or a forwarding device — continues from the receiver’s offset instead of restarting. Up to three resumes within the item’s own time budget; an unreachable device is a dead link (one quick retry, then failed), not an endless resume.
  • The iOS share sheet resumes big items too. A share of 8 MiB or more from iPhone/iPad continues from the desktop’s offset after a drop, within the time the sheet gives that device; a busy or unreachable device means the next one is tried. A share the sheet could not finish is continued by the next send of the same item.
  • One transfer row per item on both sides, kept in progress across a drop; the byte count never goes backwards, and the receiving row names the device that finished the item.
  • Every clipboard push goes through the broadcast queue — Retry from History and a browser-mode copy no longer push from the daemon’s own tick, so a slow peer cannot stall clipboard polling. A retry while sync is paused now answers «Sync is paused».
  • Big items fan out two uploads at a time, hosts first, so one slow phone does not hold every desktop’s copy.
  • A sleeping phone reads as “Asleep”, not as a degraded peer, in Peers, in /api/peers (data_plane_state: "asleep") and in clipshot doctor.
  • No Welcome Screen flash on a paired device: the first-run screen is shown only after a successful settings read says there is no token; a failed or pending read shows a spinner. The daemon answers a settings load failure with an error instead of defaults.
  • A slow in-flight copy of an item no longer blocks a second copy — a faster sender takes it over, once per item.
  • No spurious catch-up offer every 5 minutes: a peer’s periodic self-announcement is no longer read as a reconnect, so connected peers stop receiving a redundant “newest item” offer on the clock.
  • Dependencies: rustls 0.23.45 (TLS 1.3 handshake advisory); validator 0.20 drops the last idna advisory from the lock.

v0.8.82 — 2026-09-14

  • iOS Share: a raw image is tried in the extension first and handed to the app if that attempt was killed; the path lands on the pasteboard only after confirmed delivery; an own send owned the pasteboard for 60 s (superseded on main, see Unreleased). Large-file fan-in: size-scaled per-candidate timeout, one in-flight copy per item. A mesh forward never echoes to the origin; received files stay off the desktop clipboard; History Copy path works on phones; abandoned transfer rows fail instead of spinning.

v0.8.81 — 2026-09-08

  • Relay health: a node never dials its own address as a peer relay; one canonical spelling of peer-relay URLs in log, health and dial.

v0.8.80 — 2026-09-07

  • Relay health: a cancelled peer-relay attempt counts as a failure, so the 24 h quarantine is reachable from the sub-second reconnect stage.

v0.8.79 — 2026-09-07

  • Relay health: one canonical key so the 24 h quarantine actually fires (measured on 0.8.78: 87 dials/h of an unreachable peer relay, 0 quarantines).

v0.8.78 — 2026-09-07

  • Relay fault isolation: a dead relay is remembered for 24 h, peer-relay URLs are never synthesized from IP addresses, and a frozen network actor is detected and the endpoint rebuilt.

v0.8.77 — 2026-09-07

  • Removal notices wait for their host instead of dialing absent peers; a stale re-send can no longer evict a fresh clipboard frame.

v0.8.76 — 2026-09-06

  • Removal retries are bounded so clipboard and health survive a stale-peer backlog; newcomer installer and Android sharing fixes.

v0.8.75 — 2026-09-05

  • Phone share 404 fixed; any file can be shared from a phone; a received image raises a notification; Ctrl+B falls back to the newest file on disk; dead peers are evicted.

v0.8.74 — 2026-09-03

Bug Fixes

  • Headless builds trust the OS certificate store again — the fix existed, but the feature carrying it was absent from headless, the profile the Linux release binary is built with, so a node behind a corporate TLS proxy rejected the relay certificate as UnknownIssuer and sat in connecting for eight days while still reporting hub_connected: true.
  • A coalesced clipboard frame is acknowledged instead of answered “busy” — the frame that coalesces is the one that gets stored (it wins the latest-wins slot), so the sender was told to retry work that had already been accepted. A healthy mesh reported itself backpressured continuously as a result. NotEnqueued — where nothing was stored — still answers busy.
  • auto_accept_files and received_files_dir are no longer frozen at boot — a settings change read back correctly and did nothing, so a file could land on a device whose accept setting was off.

Features

  • Mobile clipboard bridge — platform-neutral POST /api/clipboard/bridge, POST /api/clipboard/bridge/image and GET /api/clipboard/bridge/pending, sharing one mailbox with the existing /api/clipboard/android spellings the shipped Android APK calls, plus POST /api/clipboard/capture and GET /api/clipboard/capture/status for platforms that cannot read the clipboard unattended. See API reference.
  • One ingest rule for both phones — the share-sheet path is stricter than the clipboard path, and the rule lives in Rust so both front ends get it.
  • A waiting file is visible on every page in the web UI, instead of History only.

Architecture

  • The OS trust store is a target property, not a per-profile feature — patching one profile had left a third one broken, with a manifest-parsing test reporting green on it.

Security

  • Dependency advisories with a patch available: 8 → 1. The remaining one (idna via validator) has no reachable call path — no email/url validators exist in the codebase.

Infrastructure

  • Build images verify what they execute — rustup-init and the bun archive are pinned and checked against upstream digests with negative tests, and the Android toolchain is pinned, so the images are reproducible rather than merely authentic.
  • CI rebuilds the Android builder image when its recipe changes, not only when the image is missing; the lane had been green about images it never ran.

v0.8.73 — 2026-08-22

Bug Fixes

  • Persistent node ID is written atomically — the node ID file was written with a truncating fs::write; a reader racing that window saw an empty file and minted a different ID, forking the host’s stable identity.

Security

  • The updater signing key and its passphrase are no longer in the repository — keys/clipshot.key and keys/.env (which held the passphrase for exactly that key) were committed together, which defeats the encryption entirely. The committed key was already superseded, not live — it derives a different public key than the one pinned in tauri.conf.json, and the live key exists only on the build host — so signed updates are unaffected. .gitignore now blocks the shape (keys/*.key, keys/*.pass, keys/.env, *.p12, *.p8) so it cannot recur.

Infrastructure

  • The release pipeline heals itself instead of stranding work — all three paths that commit into the shared working copy (release.sh, release.yml, docs.yml) now rebase-and-retry against the CI remote, up to three attempts, re-fetching each time. Previously a commit landing between the rebase and the push rejected as non-fast-forward and was swallowed as advisory, leaving the tree both ahead of and behind the remote — the state that had to be unblocked by hand before v0.8.71 and v0.8.72.
  • Only the CI remote counts as a successful push — success was judged by whichever remote happened to work, which hid the one failure that matters, since every release re-reads the CI remote.
  • The GitHub mirror is removed from the release path — nothing depended on it (install.sh downloads from clipshot.cc/dist, CI runs on Forgejo, and no artifacts were ever published there), so it was a write-only copy of the source on a third-party host.
  • Service install test asserts what ExecStart needs — it checked that the running test binary still exists on disk, which Cargo can invalidate mid-run; it now asserts the absolute path that a service manager actually requires.

v0.8.72 — 2026-08-21

Bug Fixes

  • Updates no longer wipe granted macOS permissions — reinstalling a signed build erased an already-granted Accessibility / Input Monitoring permission and re-prompted for it. The tccutil reset that caused this was correct only under ad-hoc signing, where a grant is bound to the binary’s cdhash; releases are Developer ID signed and notarized now, so a new build satisfies the existing grant on its own. The reset now fires only for a grant that is dead rather than merely absent — the binary changed and that service still reads denied. A denied service is also reset on its own, so one rotted grant no longer takes a healthy neighbour down with it.
  • Permission fingerprint is written even while a permission is denied — previously a single update was re-reported as “binary changed” on every subsequent launch.

Architecture

  • TCC reset policy moved out of the GUI — the decision, the binary fingerprint, and the service names now live in src/permissions/tcc_reset.rs instead of src/gui/tauri_app.rs; the bundle ID and the three spellings of each TCC service name (kTCCServiceListenEvent, ListenEvent, “Input Monitoring”) are single-sourced through a TccService enum. The decision is pure code, so it stays under test on Linux and Windows CI too.

Infrastructure

  • CI image recipes committed — the clipshot-docs-builder image had no recipe in the repository and existed only as a local artifact on one host, which failed the release preflight. Its Dockerfile is now committed alongside rebuild-all-ci-images.sh, which restores the exact image set the preflight checks and reports free disk against the release gate.
  • IPv4 precedence in the docs-builder image — gem install hung because the resolver kept choosing AAAA records on a host with no IPv6 egress; the same one-broken-family-must-not-starve-a-healthy-one rule the daemon applies to its own DNS.

v0.8.71 — 2026-08-21

Bug Fixes

  • Atomic node ID write — the persistent node ID was written with a truncating fs::write; a reader racing that window saw an empty file and minted a different ID, forking the host’s stable identity. The write is now atomic.
  • paste_path names files after their content type — Cmd+B / POST /api/paste_path always used the img_YYYYMMDD_XXXX.png helper, so clipboard text was saved under an image name while every other node in the mesh wrote text_….txt for the same bytes.
  • Health probe proves application liveness — the probe no longer accepts a bare QUIC path as proof that a peer is alive.
  • Relay candidates filtered by address family — relay candidates (including the primary relay URL) are filtered to the address families actually available locally, so a v4-only host stops dialing IPv6 relays.
  • Reconnect stage no longer starves or livelocks on portal candidates over high-latency links.

Architecture

  • Multistream sync handler — a persistent bounded connection supervisor keeps shared QUIC connections for peer answers, with isolated and bounded inbound file streams.

Infrastructure

  • Developer ID signed macOS builds — signing and notarization lane added, with credentials sourced from outside the repository.

v0.8.70 — 2026-08-07

Bug Fixes

  • Handshake success no longer wipes reconnect escalation state — a bare QUIC handshake was treated as “reconnected” and deleted the peer’s ReconnectState, so a peer could sit at connected_peers=0/5 while every status surface stayed green. Root cause of a 57-minute silent outage.
  • Windows pastes a received file as a file — CF_HDROP is implemented, so Ctrl+V in Explorer no longer pastes just the file name.
  • Windows clipboard CPU storm — polling now uses the clipboard change counter: 14.5s → 1.8s CPU per 30s.
  • Path traversal via peer-supplied filenames — a peer could write outside the sync directory through the content filename; both the direct and chunked receive paths now share one guarded-read primitive.

Security

  • Cross-origin and cross-host HTTP callers rejected — CORS matched origins by prefix with no delimiter check and reflected the match back, so http://localhost.evil.com was allowed to read responses. Host matching is now exact, and a DNS-rebinding gate covers every route including /health and preflight.
  • The group token is no longer served over the API — GET /api/settings returned the full group join credential. The API projection now returns [redacted:<8 hex of SHA-256(token)>]; redaction lives in the projection, not on the field, because the same struct is the on-disk TOML format.
  • History content reads confined to the sync and received directories.
  • Bounded receives — the chunked receive, the number of concurrent chunked receives, and the pending-incoming queue are all bounded, and partials are reaped; a hostile mDNS TXT record can no longer panic the daemon.
  • Received-file retention judges a file by when it arrived, not by the mtime it claims.

Infrastructure

  • Tick lock-poison recovery completed for the remaining lock set, with a panic budget for the GUI tick driver.
  • Log-noise sweep — caller-triggerable command failures are throttled across all seven paths, and several per-poll clipboard log sites were demoted to debug.

v0.8.69 — 2026-08-03

Bug Fixes

  • Windows hotkeys — removed a duplicate track_modifier_state left behind by the hotkey listener decomposition.
  • Honest degraded warnings — the outbound data-plane WARN now distinguishes entered degraded, a throttled echo with no new failures, and worsening (failure count grew), instead of printing the same line in all three cases.

Features

  • Diagnosis telemetry switch — one reusable opt-in, diagnostic_telemetry (default false), gates Clipshot’s own investigation-aid logging. Enable it via settings.toml, a native GUI full-settings save, or CLIPSHOT_DEBUG_TELEMETRY=1; it is deliberately not settable over POST /api/settings.
  • Push attempts exposed — /api/peers now reports push_attempts_since_quarantine, which separates a genuinely failing transport from a peer that is quarantined but never actually pushed to.
  • Doctor states the contradiction — clipshot doctor spells out “quarantined but still receiving data” and says whether any outbound push was attempted, instead of leaving it to journal archaeology.

Infrastructure

  • Log-noise sweep — the noq and noq_proto QUIC crates are error-capped (they were ~95% of the journal), the iroh probe-success INFO is throttled per canonical peer id (184 lines/hour from a single unreachable peer → ~12), and “Enumerated N network interfaces” dropped to debug.
  • Hotkey listener, daemon-startup, and test-hook route tables were decomposed; the CI complexity gate no longer miscounts braces inside string and char literals.

v0.8.68 — 2026-08-02

Bug Fixes

  • Per-family libc DNS resolve — libc lookups are issued separately for IPv4 and IPv6, each under its own timeout, so one broken address family no longer discards the healthy one. This removes the cause of the corp-network DoH request storm (see rule 53).
  • Peer card honesty — the peer UI no longer renders “reachable but not connected” and “dead” identically.
  • Tick lock-poison recovery — a caught tick panic now clears lock poison in both tick drivers, so one panic no longer guarantees a cascade of follow-up panics.

Architecture

  • Hub / setup decomposition — the hub run loop and Tauri setup_app wiring were split into smaller units (pure extraction, no behavior change).
  • Bare no-iroh build rejected explicitly — cargo build --no-default-features without iroh now fails with a clear compile_error! instead of a wall of confusing type errors.

Infrastructure

  • Log records are tagged with their originating crate, and noisy dependency crates (including hickory_net) are capped through one shared list.
  • CI complexity and formatting gates are green tree-wide.

v0.8.67 — 2026-07-28

Features

  • Always-on crash diagnostics — release binaries keep symbols so backtraces are readable, fatal signal handlers (plus a Windows exception filter) capture crashes, a two-tier signal-safe breadcrumb ring records recent activity, and a heartbeat snapshot acts as a SIGKILL witness. clipshot doctor surfaces the collected crash evidence.

v0.8.66 — 2026-07-25

Bug Fixes

  • Outbound data-plane liveness — outbound clipboard-push failures are tracked separately from health RTT, quarantined peers are evicted and re-seeded for reconnect, and /api/peers exposes data_plane_state and consecutive_push_failures. Health can no longer look green while pushes silently wedge.
  • Receiver stuck-inbound liveness — inbound drain stalls are detected and surfaced as consecutive_inbound_stalls; accept_bi and payload reads are bounded, with a latest-wins bounded hand-off and ACK-after-enqueue.
  • Relay-hint reconciliation — relay candidates now union relay_url with the verified relay, dial multiple relays, and fall back on the data-plane push path.
  • Identity domain split — a two-layer canonicalizer replaced the synthetic mesh-u64 identity mint, fixing GUI share-URI silent peer loss.

Features

  • Refresh all — force-reconnect every dropped peer from the UI.

v0.8.65 — 2026-07-19

Bug Fixes

  • Portal RemoveDevice retried until hub-confirmed, so device removal converges even if the hub is temporarily down.
  • Gossip re-adoption honors tombstones, so a removed device is no longer re-adopted through gossip.

v0.8.64 — 2026-07-19

Features

  • Device removal converges across the fleet — new PeerRemovedV2 wire message (append-only, dual-send), split last-writer-wins on receive (gossip vs authoritative), local tombstone store, remove-intent retry set, snapshot removed-list ingest, a data-plane inbound gate for clipboard and file traffic (with kill-switch), victim-side UX, and a doctor check.

v0.8.63 — 2026-07-18

Bug Fixes

  • Peer-remove mesh-stall fix pack — endpoint rebuild now respawns all workers, the expire-vs-backoff invariant is enforced, removal propagates without the hub (with a source_id auth gate), removal purges every reconnect source, a projection-contradiction guard was added, node_id persists as a hex string (values above i64::MAX no longer break), and a control-frame flush bug was fixed.
  • CLI clipshot setup notifies a running daemon of the newly adopted token.

Features

  • Tauri auto-updater — signed update artifacts and an update manifest are produced by the release lane.

Infrastructure

  • New test-only route GET /api/test/reconnect_state for reconnect forensics.
  • Releases auto-mirror to GitHub.

v0.8.62 — 2026-07-15

Infrastructure

  • macOS GUI DMG built in CI — the .app bundle + DMG installer (Clipshot_VERSION_aarch64.dmg) is now produced by the release pipeline in a headless KVM macOS VM (macos-vm.sh build-gui); warn-only for the first release, then gating.
  • Repo history purged of committed binaries — 218 MB → 17 MB pack; dist/* no longer tracked in git/LFS.
  • install.sh aligned with the DMG lane — macOS default install downloads the DMG, mounts it, copies the .app to /Applications, and clears quarantine (xattr -cr).

v0.8.61 — 2026-07-15

Bug Fixes

  • Single token-write lock — all group-token writes go through one leaf lock, closing the C4 class of racy token overwrites.
  • CLI clipshot setup token adoption fixed (B4) — setup now uses the store-level token-replacement path instead of the disk-preserve settings merge that silently kept the old token.

Infrastructure

  • Docs annotator fails loudly + stable data-testid selectors for screenshot automation.
  • Dispatch-only release pipeline — workflow_dispatch is the single release trigger.

v0.8.60 — 2026-07-15

Infrastructure

  • Docs screenshots pipeline — version badge rendered in browser mode (no more permanent v?), declarative annotated callouts via annotations.json, and readiness-waited screenshots to eliminate half-loaded captures.

v0.8.59 — 2026-07-15

Features

  • Browser sign-in in the GUI — the Welcome screen and Settings page can open a browser setup session (clipshot setup flow): sign in with email/password, Google, or GitHub, pick a group when you belong to more than one, and the app adopts the group token without a restart.
  • replace_group_token primitive — intentional token replacement (Tauri command + POST /api/settings/replace_group_token) that bypasses the disk-preserve settings merge and reconnects the hub on the new token (fixes B4/B5/B10, including re-pair returning a node to its old group after restart).

Security

  • Popup/tabnabbing hardening — browser sign-in opens the window synchronously in the user-gesture stack with noopener,noreferrer, with a copyable fallback link when popups are blocked.

v0.8.58 — 2026-07-14

Bug Fixes

  • macOS post-sleep mDNS spin fixed — after sleep/wake, mDNS sockets returning ENOTCONN caused a hot spin: 100% of one core, log storm, thousands of leaked file descriptors → EMFILE → DNS failures → the node dropped off the hub (“loses connection once a day”). Fixed by upgrading mdns-sd 0.11 → 0.20 (two sockets total, no fd bomb) plus a pure sleep-gap watchdog (src/network/sleep_gap.rs) that detects wall/monotonic clock gaps > 30 s and recreates the mDNS registration after wake.

v0.8.57 — 2026-07-07

Bug Fixes

  • System-nameserver DNS rung — the external-DNS fallback layer now queries the nameservers from /etc/resolv.conf directly over UDP/53 before DoH. Covers hosts where the libc/NSS layer is broken but the host’s DNS infrastructure is healthy, and where corp MITM proxies kill HTTP/2 DoH (closes the gene corp-DNS incident). Only nameserver IPs are taken — never search/ndots options.

v0.8.56 — 2026-07-07

Features

  • External DNS policy — corp-network-resilient resolution for external endpoints (hub WebSocket, pair portal): libc first (1.5 s, respects /etc/hosts), then hickory with DoH fallback (8 s), then a verified-IP cache (~/.clipshot/dns_cache.json, TTL 7 days). Layer selection escalates on connection failures, not just resolve failures.
  • Doctor: external DNS health check — verdict matrix over libc/DoH-fallback/IP-cache probes with kill-switch-aware wording.
  • Kill-switch — external_dns_fallback = false in settings.toml disables the DoH layer (for compliance-locked corp devices); not settable via HTTP.

v0.8.55 — 2026-06-23

Infrastructure

  • Test-suite stability — eliminated parallel global-state races in cargo test --lib (60/60 clean parallel runs); more Docker E2E lanes (pair-v2, mesh-heal, file-transfer, mutation pilots, iroh-restart) promoted from advisory to gating.
  • One-command release script + repo cleanup.

Bug Fixes

  • Faster, non-dead-end reconnect for isolated/relay-less peers.

v0.8.54 — 2026-06-20

Infrastructure

  • Full CI/CD pipeline on self-hosted Forgejo — unified release.yml with 5 chained jobs: test → build → docs → deploy → verify. All builds run in Docker containers on spex (no external CI).
  • Windows headless binary added to release artifacts (clipshot-windows-x64.exe via Docker mingw cross-compile).
  • Docs auto-audit pipeline — AI-powered docs audit → fix → screenshot refresh → Jekyll build → deploy to docs.clipshot.cc, all containerized.
  • Self-hosted downloads — all download URLs point to clipshot.cc/dist, zero GitHub dependency.
  • Push-to-main CI trigger — Forgejo Actions now auto-runs tests on every push to main.

v0.8.53 — 2026-06-01

Bug Fixes

  • Copying a file/image no longer hijacks the clipboard — previously, copying a file in Finder could replace the clipboard with a text sync path, so pasting into a chat app inserted a path instead of the file/image. Now the clipboard is never auto-overwritten: Cmd/Ctrl+V pastes the original content, and Cmd+B / Ctrl+B writes the sync path on demand. Behavior is uniform across macOS/Linux/Windows — no Finder-specific special case.
  • Headless paste_path (Ctrl+B) now correctly persists the path write — a missing clipboard detach() previously let the spawned xclip die, losing the write.

Architecture (SOLID/DRY/KISS)

  • Removed the entire auto-overwrite decision layer: sync::clipboard_paste_policy module, Clipboard trait methods is_finder_copy / supports_multi_type_write / set_image_with_path_text, ContentKind/ContentType::kind(), and the paste_sync_path_on_file setting. Net −535 lines. Clipboard write path is now one uniform rule on every OS.

Tests

  • New unit tests pinning the contract: sync_content never overwrites the clipboard for image or file content, path always cached for Cmd+B.
  • E2E (Docker/xclip): clipboard-toggle-test.sh rewritten to the current contract.
  • E2E (macOS/NSPasteboard): new macos-finder-copy-test.sh — Finder file copies stay as files, images stay as images, Cmd+B writes the path on demand.

v0.8.52 — 2026-05-29

Bug Fixes

  • Multi-file copy no longer breaks the clipboard — copying 2+ files in Finder/file manager previously read only the first file (rest silently lost) and then overwrote the clipboard with a single sync path, destroying the whole selection. Now a multi-file copy is left completely untouched: no sync, no clipboard rewrite. Single-file copy behavior unchanged. To send several files, archive them into one.

Architecture (SOLID/DRY/KISS)

  • FileClipboardOutcome value type — content_from_files now returns Empty | Single | Multiple instead of Option. One pure decision point (SRP), reused identically by all four platform clipboards (macOS/Wayland/X11/Windows) (DRY). Multiple short-circuits get_content() to NoImage without falling through to the image probe (avoids syncing Finder’s icon-preview PNG).
  • Multi-file judged by input length, not readability — a broken file in a 2-file selection cannot collapse into a single-file sync.

Tests

  • 7 new unit tests for the file-outcome policy + a Linux/X11 docker E2E verifying multi-file leaves clipboard + sync dir untouched while single-file syncs.

v0.8.51 — 2026-05-25

Architecture (SOLID/DRY/KISS)

  • Relay hostname preserved — removed DNS pre-resolve that broke TLS/SNI (relay.clipshot.cc → raw IP). New RelayConfig value object.
  • Runtime defaults centralized — runtime_defaults.rs for P2P/HTTP/test-hooks ports. Zero hardcoded ports in runtime code.
  • DaemonClient IPC — ipc/daemon_info.rs + ipc/client.rs. Daemon writes JSON at startup, CLI uses it for pair/status/peers.
  • install.sh delegates to binary — clipshot service install renders systemd/launchd. One source of truth.
  • peer_registry SRP split — 2375 → 1153 impl lines across mod.rs (553), addr.rs (467), metadata.rs (133).
  • iroh_addr module — pure address helpers extracted from PeerRegistry.
  • Unified connection check — is_peer_alive() replaces duplicated manager && monitor pattern.

Bug Fixes

  • aws-gene connectivity restored — relay domain preserved for TLS/SNI.
  • Clipboard: Cmd+V pastes content, not path — only Finder Cmd+C overwrites clipboard with sync path. Screenshot/Telegram/Slack images stay as images. Detection via com.apple.finder.noderef.
  • CLI clipshot pair polls for joiner — shows code, waits 5 min with progress dots, auto-confirms.
  • install.sh pairs AFTER daemon starts — was attempting pair before service installed.
  • Flaky tests fixed — serial(test_hooks_env) for CLIPSHOT_TEST_HOOKS env races.

UI

  • PairDialog: OS install picker — Linux 🐧 / macOS 🍎 / Windows 🪟 tabs with platform-specific install commands.
  • History: copyable path field — read-only input with select-all and copy button under each transfer.
  • Device badge overflow — long node names truncate with tooltip, latency stays inside bounds.

Testing

  • 1872 Rust unit tests (was 1849), 299 Vitest, 27 E2E tests passed.
  • 3/3 parallel cargo test runs clean (0 flaky).

v0.8.50 — 2026-05-25

Architecture: DaemonClient IPC

  • New src/ipc/ module: DaemonClient connects CLI commands to the running daemon via localhost HTTP API
  • DaemonInfo (pid, version, http_port) written to ~/.config/clipshot/daemon.json at daemon start
  • CLI pair, status, peers, settings commands now use DaemonClient instead of control socket
  • src/runtime_defaults.rs centralises port constants: P2P_PORT (19231), HTTP_PORT (15282), TEST_HOOKS_PORT (18181)

Clipboard paste policy (KISS)

  • Never auto-overwrite clipboard with sync path — except Finder file copies
  • Finder Cmd+C detected via com.apple.finder.noderef pasteboard type
  • All other sources (screenshots, app copies, images) keep original clipboard content
  • Cmd+B still gives the sync path on demand
  • New module: src/sync/clipboard_paste_policy.rs (pure policy, fully tested)

CLI pair now polls for joiner

  • clipshot pair (generate) now polls for up to 5 minutes, showing progress dots
  • Displays confirmation digits when joiner arrives
  • No longer fire-and-forget — mirrors the GUI PairDialog UX
  • install.sh pairs AFTER daemon starts (daemon must be up for DaemonClient IPC)

PairDialog OS install picker

  • After generating a pair code, the dialog shows install commands for Linux, macOS, and Windows
  • Platform tabs with one-click copy: curl | bash --code=... (Linux), brew install ... && clipshot pair (macOS), irm | iex (Windows)

Refactoring

  • peer_registry split into SRP modules: mod.rs, addr.rs, metadata.rs, tests.rs
  • iroh_addr.rs extracted from PeerRegistry: pure stateless iroh URL utilities
  • relay_config.rs preserves configured relay hostname for TLS/SNI
  • Unified connection alive check (DRY)
  • Daemon auto-enables HTTP API on port 15282

Fixes

  • Image clipboard keeps PNG, not path (Cmd+V pastes image again)
  • Device badge overflow — truncate name, keep latency inside bounds
  • Path field overflow in UI
  • Clippy clean, flaky test fixes (serial test_hooks_env)

v0.8.49 — 2026-05-25

File size limits removed

  • No hard file size limits on file transfer (Pro) — any size works
  • Clipboard sync max_file_size_mb setting default unchanged (10 MB), configurable up to 10 GB
  • Removed stale 1 MB / 1 GiB size limit references from docs and types

v0.8.48 — 2026-05-24

New: File Transfer (Pro)

Targeted file transfer to specific peers via drag-and-drop UI.

  • Send files of any size to selected peer(s) from sidebar “Send File” dialog
  • Chunked streaming for files ≥50MB — streams from disk in 1MB chunks, no full-file memory load
  • Resume on disconnect — receiver saves partial file, sender continues from offset on retry
  • Sequential delivery — one peer at a time, reorder priority in History page
  • Auto-accept (default) or manual approval mode via Settings
  • System notifications on file sent/received/pending
  • Pro gate — sending requires Pro subscription; receiving works for all
  • Hash-verified — FNV-1a hash checked after transfer, mismatch = retry

File Transfer API

  • 7 new Tauri commands: send_file, get_file_transfers, reorder_file_transfer, cancel_file_transfer, accept_incoming_file, decline_incoming_file, get_pending_incoming_files
  • 7 new HTTP endpoints: /api/file/{send,transfers,pending,reorder,cancel,accept,decline}
  • Wire protocol: MSG_FILE_TRANSFER (0x03) on existing QUIC ALPN
  • 2 new settings: auto_accept_files, received_files_dir

Fixes

  • Clear stale connection state after iroh endpoint rebuild (production spex crash-loop fix)
  • Self-relay detection — skip custom relay when it resolves to local interface
  • BroadcastWorker writes sync_sent activity events (history was empty for sent items)
  • Partial file truncation on fresh transfer (no append to stale data)
  • ACK resilience for large transfers — treat connection-lost-after-all-bytes-sent as success

v0.8.46 — 2026-05-22

Critical: broadcast delivery fix

v0.8.42→v0.8.45 cleanup introduced address URI pollution that caused all broadcasts to timeout (15s × 4 peers = 60s stuck in Sending).

Root cause: Two URI encoding forms (comma addrs=A,B,C vs ampersand addrs=A&addrs=B&addrs=C) coexisted in peer_registry.addresses[]. best_iroh_addr() picked the longest (most polluted) form → iroh Endpoint::connect() timed out on malformed addresses.

Fixes:

  • canonicalize_iroh_url() — deterministic sorted comma form for all iroh URIs. Parses, deduplicates, sorts SocketAddr, re-encodes.
  • best_iroh_addr() — semantic scoring (relay > addr count > shorter) instead of max_by_key(len) which always selected the worst form.
  • merge_address_by_node_id() — canonical comparison prevents Form A/B duplicates from accumulating.
  • add_peer() — canonicalize all addresses on ingest including the original address from peers.toml.
  • replace_addresses_by_node_id() — canonicalize hub-authoritative addresses so sorted-vs-unsorted forms collapse.
  • build_connect_addr_for_peer() — rewritten to use best_iroh_addr + relay enrichment (old code re-extracted addrs from all entries, which was the source of Form B pollution).

Transport & architecture cleanup (55 commits)

  • Replace IrohAddrTracker with native Endpoint::watch_addr() (−599 LOC)
  • Drop dead Ping/Pong sender path → use Connection::rtt() (−97 LOC)
  • Set QUIC keep_alive_interval=10s + max_idle_timeout=45s
  • LivenessSupervisor dynamic membership — per-peer IrohPeerChannel registered on every hub peer-add path
  • Respawn supervisor on hub reconfigure
  • rebuild_clipshot_router() — decouple Router from Endpoint lifecycle
  • iroh::Endpoint::metrics() exposed via /api/test/iroh_metrics
  • Connection::paths() exposed via /api/test/connection_paths
  • Wire MemoryLookup AddressLookup populated from hub events
  • 5s read-through settings cache for hot-path loaders
  • Fold ContentTypeKind into canonical ContentKind
  • Collapse PasteboardSource enum into file_takes_priority(bool)
  • Clipboard cache populated on auto-overwrite (Cmd+B toggle-back fix)
  • Serialise ensure_group_token with PAIR_V2_SETTINGS_LOCK

Diagnostics

  • /api/test/peer_registry_dump — address pollution diagnostics
  • /api/test/iroh_metrics — full EndpointMetrics via iroh registry
  • /api/test/connection_paths — per-peer connection path snapshots

Docker E2E

  • docker-compose.regression-v846.yml — 3-node mesh (1 GUI + 2 headless)
  • 6 Playwright tests: mesh formation, address dedup invariant, broadcast delivery, clipboard delivery, sustained traffic dedup, no duplicate names

v0.8.42 — 2026-05-21

macOS clipboard priority fix

Copying a file in Finder (Cmd+C) now syncs the real file instead of the file’s icon preview. Previously, Finder ships file URL + PNG icon in the pasteboard simultaneously, and MacOSClipboard::get_content picked the image (always present) before the file URL — resulting in a 153 KB icon being synced instead of the 1.2 MB JSON the user actually selected.

Fixed by extracting the priority into a pure-policy module src/clipboard/macos_priority.rs:

  v0.8.41:  Image > Files > Text  ❌
  v0.8.42:  Files > Image > Text  ✓

SOLID / TDD: 6 RED-first unit tests for the priority function cover the exact regression case (finder_file_copy_picks_files_not_image_or_text) plus the five neighbouring cases (screenshot, plain text, image+alt, empty, files+text).

Transport resilience unified — single LivenessSupervisor

Internal refactor (no breaking changes u2014 patch bump): replaces six independent watchdog mechanisms (hub-WS idle timeout, F1 connect timeout, tick-stall watchdog, iroh_listener_dead flag + accept-loop detection, HealthWorker poll loop, ad-hoc reconnect commands) with one primitive — see plans/transport-resilience-unified.md.

What’s new

  • src/p2p/v2/transport_health/:
    • Heartbeat + Restartable traits (async_trait, object-safe)
    • Policy struct (single tick_interval + probe_timeout + consecutive_failures_before_restart + cool_down_after_restart, matched to v0.8.24 production tuning by default)
    • LivenessSupervisor + SupervisorMetrics (OnceLock global)
    • 3 concrete adapters:
      • HubWsChannel — observes HubClient::last_recv + signals reconnect via HubClient::request_reconnect
      • IrohRelayChannel — absorbs iroh_listener_dead flag into the unified API
      • IrohPeerChannel — naturalises HealthState::failures (one channel per peer; escalates only at chronic failure count)
  • GET /api/test/transport_health — single unified diagnostic surface (HTTP API + Mac Tauri test-hooks at port 18181). Replaces 4 legacy endpoints (/api/status:hub_connected, /api/peers:latency_ms, iroh_listener_dead flag, journalctl scraping).

What got removed

  • HubClient::run_loop inline idle_timeout = 120 s check (now owned by the supervisor’s Policy)
  • F1’s standalone CONNECT_TIMEOUT constant is now scoped to the connect call only (no longer a public knob)

Tests

  • cargo test --lib: 1517 → 1548 (+31 new tests)
  • New tests/e2e/scripts/transport-chaos/ Docker chaos suite:
    • C1 udp-blackhole-peer
    • C2 sleep-wake
    • C3 relay-restart
    • C4 hub-blackhole
    • C5 multipath-confusion
    • C6 portal-restart
    • C7 24-h passive soak (scheduled CI weekly)
  • New .github/workflows/transport-chaos.yml runs C1-C6 fast (≤10 min) on every push touching transport code; C7 runs Sundays 02:00 UTC.

SOLID/DRY/KISS pay-off

  • SRP: probe / classify / decide-restart are now three separate concerns in three different layers (HealthState/HubClient → adapters → LivenessSupervisor).
  • DRY: 86 grep hits on watchdog/idle_timeout/CONNECT_TIMEOUT/ last_alive across src/p2p/v2/ in v0.8.24, down to ≤ 12 hits in v0.9.0 — all inside transport_health/.
  • KISS: adding a new transport (future BLE, WebTransport, …) requires implementing only Heartbeat + Restartable — no edits to the supervisor, no new global constants, no new timeout knobs.
  • TDD-strict: every T-step (T0-T7) was a discrete commit, each leaving the tree green. RED scenarios committed at T1 before any supervisor existed; each subsequent T-step turned a defined subset green.

Carry-over from v0.8.24

  • F4 regression test rewritten as test_request_reconnect_breaks_inner_loop (supervisor-level behaviour moved to chaos C4).
  • F5 test-hooks endpoints (/api/test/status, /api/test/peers) kept.

v0.8.24 — 2026-05-20

Hub WS regression coverage + Mac production debug

Completes the F-series begun in v0.8.23. The earlier release shipped the actual self-healing fixes (F1 connect timeout, F2 TCP keepalive, F6 boot-time peer-registry resync); v0.8.24 closes the remaining two items in the plan: a regression test for the production failure mode, and an on-Mac debug endpoint so the next incident is diagnosable without restarting the daemon with --browser.

  • F4 — post-handshake silence integration test. New test_post_handshake_silence_triggers_reconnect in src/p2p/v2/hub/connection.rs::tests. Spawns a mock WS server that accepts the upgrade, reads Hello, then goes silent forever (exactly the it.local 2026-05-20 failure mode). With start_paused = true, the test advances tokio’s virtual clock past the 120 s idle timeout and asserts the daemon emits both Connected (handshake worked) and Disconnected (silence detected, reconnect loop will retry).

    Required one surgical source change to make the integration testable: connection.rs now imports tokio::time::Instant instead of std::time::Instant. The two are bit-identical in production (both are monotonic-clock samples) but only the tokio variant advances with tokio::time::pause() / advance(). No behavioural change at runtime; substantial test-coverage win for the entire heartbeat / idle / reconnect path.

  • F5 — Mac Tauri test-hooks for /api/status + /api/peers. New routes in the existing CLIPSHOT_TEST_HOOKS server in src/gui/tauri_app.rs:

    • GET /api/test/status → cached DaemonStatusFull JSON
    • GET /api/test/peers → cached Vec<FullPeerInfo> JSON Both read from crate::p2p::control::cache — the same source the production /api/status and /api/peers HTTP routes serve. Mac Tauri-GUI mode does not bind tiny_http on 15282 unless use_browser_ui = true; with CLIPSHOT_TEST_HOOKS=1 (default port 18181) operators can now curl http://127.0.0.1:18181/api/test/status on it.local to diagnose hub / peer state without restarting the app or enabling the full browser-mode HTTP API. Tests assert GET-only and route-string distinctness.

Notes

  • 1517 lib tests (1514 → 1517, +3 new for F5 route predicates plus the F4 integration test).
  • No new runtime dependencies.

v0.8.23 — 2026-05-20

Hub WebSocket self-healing (production incident 2026-05-20)

it.local Mac daemon stalled for 4+ hours with a single ESTABLISHED TCP socket to the portal and zero TLS handshakes completing. The reconnect loop was blocked inside tokio_tungstenite::connect_async_tls_with_config because the call had no timeout and inherited the OS-default TCP timeout (effectively unbounded on macOS without keepalive).

Three-layer fix:

  • F1 — connect timeout (15 s). Wraps the WS handshake in tokio::time::timeout so a stalled handshake aborts within seconds and the existing exponential backoff retries from scratch. Tests: test_connect_timeout_is_bounded_for_half_open_resilience, test_stalled_tls_handshake_does_not_hang_forever.

  • F2 — TCP keepalive (30/10/3). New src/p2p/v2/hub/keepalive.rs module opens TCP via socket2, arms SO_KEEPALIVE + TCP_KEEPALIVE*, then hands the socket to tungstenite. Worst-case half-open detection on Linux: 60 s. macOS lacks TCP_KEEPINTVL/TCP_KEEPCNT so it falls back to the application-level 120 s idle timeout already in run_loop. New dep: socket2 = "0.5". Tests: 6 in keepalive::tests.

  • F6 — boot-time peer-registry resync. New AsyncDaemon::apply_authoritative_iroh_addr collapses peer_registry.addresses[] to the single hub-authoritative iroh addr on every ProcessPeerList / ProcessPeerJoined / PeerNodeAddrChanged event. Eliminates the address accumulation that previously left daemons trying dead UDP ports on the first reconnect attempt. Tests: 4 in apply_authoritative_iroh_addr_*.

Other

  • Pinned socket2 = "0.5" with feature all for cross-platform TCP keepalive accessors.

Net change: 1513 lib tests (1497 → 1513, +16 new). All pass.

v0.8.22 — 2026-05-17

Peer relay connectivity (6 layers fixed)

Connection through embedded peer relays now works end-to-end. Previously, behind-NAT nodes (main, gene) could not reach each other even when a publicly-routable peer (spex) was online with relay-server enabled — the relay address never made it from the publisher to the consumers.

  • L1 — Relay DNS pre-resolved at startup: relay URL is resolved once on boot, so subsequent broadcasts never block on DNS even on Tailscale/VPN paths where the resolver is unreachable.
  • L2 — headless cargo feature includes relay-server: every headless daemon (CLI / server) can now act as a peer relay. Previously only GUI builds shipped the relay, which broke server-as-relay topologies.
  • L3 — persist_hub_peer stores relay_addresses: relay addresses advertised by other nodes via the hub are now persisted to the local peer registry instead of being dropped after the WS frame is decoded.
  • L4 — persist_hub_peer merges addresses when a peer changes ports / IPs, so we don’t lose the relay entry on the next hub broadcast.
  • L5 — ping_peer_by_id (no DNS, no relay lookup): HealthWorker now pings peers by EndpointId and reuses the iroh path cache, fixing health timeouts on Tailscale where DNS for relay.clipshot.cc is blocked.
  • L6 — Portal persists + broadcasts relay_addresses (KEY FIX): the hub previously hardcoded relay_addresses: vec![] in every PeerList / PeerJoined message, so consumers never learned about the publisher’s relay even though the publisher reported it correctly. Migration 018 stores it; the WS layer now reads and broadcasts it.

End result: spex ✓ direct, main ✓ via spex relay, gene ✓ via spex relay.

HealthWorker adaptive backoff

Offline peers (e.g. Windows VM turned off) used to be pinged every 30 s forever — wasting CPU, flooding logs, and making the UI flap between ✓ and timeout. Health probes now back off after sustained failure:

consecutive failures next-probe wait
0..3 30 s (base)
3..10 5 min
10+ 15 min

A successful probe — or a hub peer_joined event for that node — resets the counter, so a returning peer is rediscovered within one cycle (≤ 30 s) instead of waiting out the long backoff window. The 1–2-failure “flaky peer” guard is preserved by regression test.

Other

  • macOS clipboard: received files now write through NSFilenamesPboardType so Finder paste works for transfers, not just text/images.

v0.8.21 — 2026-05-13

Unified Device Removal

  • PeerRemoved P2P gossip — remove a peer from any node, all connected peers learn via native QUIC
  • Hub RemoveDevice — removal propagates through portal to all group members
  • PeerRemovedFromGroup — distinct hub message for explicit removal (vs soft disconnect)
  • No gossip loop — receivers remove locally without re-broadcasting
  • Portal dashboard delete — “Remove” button deletes node + broadcasts to all peers
  • device_id canonical identity — portal deduplicates by device_id, not node_id

Security

  • SQL injection fix — portal remove_device uses parameterized queries (was format!)

Pair Flow Fixes

  • pair_confirm hub reconnect — generator connects to hub after writing provisional token
  • reconnect_hub reads settings — fills missing hub_url/token from settings.toml

UI

  • Tray menu “Manage Account” — replaces “Upgrade to Pro”, checks is_pro from hub cache
  • Onboarding overlay — 4-slide guide after first pair (Cmd+B comparison table)

Documentation

  • Cmd+B / Ctrl+B paste path hotkey fully documented
  • macOS permissions — Input Monitoring + Accessibility requirements
  • Registration flow — setup, OAuth, dashboard, link account, Free vs Pro
  • Quick Start Guide — first section of USER_GUIDE

Tests

  • Rust: 1844, Vitest: 299, Portal: 35
  • E2E: 23 scenarios (pair, sync, remove, portal, collisions, dedup)

v0.8.2 — 2026-05-12

Group Token Flow

  • group_token in PairOffer: joiner adopts generator’s token automatically
  • Deferred token write: pair_generate holds token in memory, persists only on pair_confirm (prevents UI flash)
  • group_switch warning: PairDialog shows yellow banner when joining a different group
  • mDNS carries group_token in TXT record for LAN pairing
  • Hub without registration: any non-empty token gets hub + relay access (anonymous group auto-created)

Portal

  • link-group updates anonymous groups: claims ownership on account registration
  • link-group creates group row: dashboard shows group immediately after link
  • Setup page accepts existing token: pre-fills from URL param
  • Dashboard token card: prominent “YOUR GROUP TOKEN” with copy button after login

UI Improvements

  • Loading spinner in AppContent before settings resolve (no more WelcomeScreen flash)
  • Copy button for group_token in Settings page
  • TransferIndicator in header: shows active transfer progress (↑ img.png 439KB/1.2MB)
  • connection_type detection: relay vs direct based on address pattern
  • device_type inference: server/desktop heuristic from peer name
  • Transport labels: “Relay · QUIC” / “Direct · QUIC” in peer details
  • Latency display: shows “—” instead of “Measuring…” when unavailable

Infrastructure

  • install.sh: –code optional, –token flag for direct token setup, v0.8.2 banner
  • Windows NSIS installer: 5.1MB installer with Start Menu shortcuts + uninstaller
  • broadcast_queue_size: now settable via HTTP API (validation 1-10)
  • RTT consolidation: ConnectionMonitor is single source of truth, 47 lines dead code removed

Tests

  • Rust: 1616 → 1634 (+18)
  • Vitest: 247 → 262 (+15)
  • Portal: 35

Changelog

[0.8.1] - 2026-05-12

🐛 Relay Reconnect Resilience

  • Central relay (relay.clipshot.cc) is no longer permanently blacklisted after temporary downtime
  • Relay connect attempts increased from 3 to 5 with exponential backoff (2s→4s→8s→16s = 30s window)
  • Periodic health reset every 5 minutes ensures central relay is always retried
  • Peer relays still blacklisted after repeated failures (prevents hot-loop on dead peer relays)

✅ Verification

  • 1612 Rust tests pass, clippy clean
  • RED→GREEN TDD: test_reset_failures_clears_counter, test_connect_via_relay_does_not_blacklist_central_relay

[0.8.0] - 2026-05-11

🚀 Unified Pairing (pair-v2) — Breaking Change

Complete rewrite of the pairing subsystem. One numeric 6-digit code replaces all previous pairing mechanisms (portal WORD-WORD-NN codes, LOCAL_WORD_NN LAN codes, tabbed UI). Pairing no longer requires a Portal account — group_token is generated locally at first pair and accounts can be linked later.

New pairing API surface

  • POST /api/pair/generate → {code, digits, expires_in_secs}
  • POST /api/pair/join → {digits, peer_addr}
  • POST /api/pair/abort → best-effort cancellation after confirmation mismatch
  • Tauri commands: pair_generate, pair_join, pair_abort
  • CLI: clipshot pair / clipshot pair 482917 (6-digit numeric)

New pairing architecture

  • src/pair_v2/ module tree: code.rs, digits.rs, offer.rs, handshake.rs, broker.rs, runtime.rs
  • PairBroker races multiple discovery transports (mDNS + Portal /api/pair/v2)
  • PairDiscovery trait with InMemoryDiscovery, MdnsDiscovery, PortalDiscovery implementations
  • X25519 keypair + DH shared secret → 4-digit confirmation digits for MITM detection
  • ensure_group_token() generates local UUID at pair time (account-decoupled onboarding)

Removed legacy pairing

  • Deleted src/pair.rs and src/http/handlers_local_pair.rs
  • Removed Tauri commands: pair_generate_code, pair_join_code, pair_generate_local, pair_join_local
  • Removed HTTP routes: /api/local-pair/generate, /api/local-pair/join
  • Removed tabbed PairDialog UI (Pair Code / Local Pair / Scan LAN tabs)
  • Removed joinOnly, defaultTab props from PairDialog/AddPeerDialog
  • Removed discover tab from AddPeerForm

UI changes

  • PairDialog: 2-button menu (Generate code / Enter code) → generate/join/verify flow
  • WelcomeScreen: primary CTA is “Connect your first 2 devices” (no create-account)
  • SettingsPage: “Sign in to link this group” action when group_token exists
  • AddPeerForm: flat layout with pair code + advanced section (paste URI / manual)

Portal side (clipshot_portal)

  • POST /api/pair/v2 + GET /api/pair/v2/:code (publish/resolve pair offers)
  • POST /api/account/link-group + GET /api/account/groups (deferred account linking)
  • Legacy /api/pair preserved for backward compatibility (pending removal window)

🐛 Resilience fixes

  • Iroh listener auto-restart: detect socket closure reactively via endpoint.is_closed() instead of waiting for the 120s watchdog stall.
  • Cmd+B echo loop: paste_file_path no longer triggers a duplicate broadcast of the same image.
  • mDNS self-discovery pollution: address validator rejects loopback, unspecified, x.x.x.0 and port 0.
  • Peer registry one-time GC: invalid legacy entries dropped at PeerStore::load time.
  • Peer-relay blacklist: find_peer_relay_url skips known-dead relays.

✅ Verification

  • Rust: cargo test --lib — 1603 passed, cargo clippy --all-targets -- -D warnings clean.
  • Frontend: 247 vitest tests passed, tsc --noEmit clean.
  • E2E: pair-v2-portal-test.sh passes (code exchange + digit parity + joiner connectivity).
  • E2E: pair-v2-lan-test.sh SKIP (Docker mDNS multicast limitation).

[0.7.6] - 2026-05-10

🚀 Connectivity Reliability

  • Central relay_url is now stored separately from direct addresses and is never auto-cleared.
  • Bare iroh://<node> addresses are rebuilt into full relay-aware connect URLs at connection time.
  • Connection-refused cleanup now removes only stale direct addresses, preserving the central relay path.
  • Removed the “hub down → strip relay” behavior that made relay peers disappear after transient hub outages.

⚡ Broadcast & UX

  • Detached BroadcastWorker sends clipboard updates outside the tick loop, so slow peers no longer block local clipboard polling.
  • Tray/Dock Sending state appears immediately on copy and returns to Idle after the first confirmed peer delivery.
  • Broadcast uses live peer cache, including hub-discovered/transient peers.
  • Large screenshots and files continue sending even when one relay peer is slow or temporarily unavailable.

🐛 Fixes

  • Fixed spex/main/gene visibility loss caused by stale bare Iroh addresses.
  • Fixed idle UI flicker where sync-proven devices disappeared after TTL despite the next relay sync succeeding.
  • Fixed stuck Sending indicator when a slow relay peer never completed.
  • Fixed reconnect state honesty: Online reflects real sync delivery instead of handshake-only success.
  • Extended stale connection expiry to reduce idle UI flapping.

✅ Verification

  • 1527 Rust lib tests passing.
  • Clippy clean with -D warnings.
  • Production test: macOS → gene/main/spex all confirmed via native QUIC relay/direct broadcast.

[0.7.5] - 2026-05-09

🚀 Install Without Pair Code

  • curl -fsSL https://clipshot.cc/install.sh | bash — works without --code
  • First-time install opens browser for account creation (clipshot setup)
  • After install shows unique pair code for adding second device
  • Portal: Setup Sessions API (POST/GET /api/setup/sessions)

🔐 Pair Code Dictionary Expanded

  • 64 → 256 adjectives, 64 → 256 nouns, 90 → 990 numbers
  • 64.8M unique codes (was 368K) — near-zero collision risk
  • Zero overlap between adjective and noun word lists
  • Retry on collision (PRIMARY KEY constraint)

🐛 Bug Fixes

  • Relay fixed: container must be on bundle_spex Docker network
  • Reconnect hot loop: on_connected() called after connect_to_addr()
  • Canonical + full addr both registered in ConnectionMonitor
  • Removed WORD-WORD-00 placeholder from all user-facing docs

📚 Documentation

  • Compact screenshots (1000×700), fresh annotations
  • Deploy skill updated: Windows on main, dual landing paths, relay instructions
  • install.sh shows <YOUR-CODE> not WORD-WORD-00

[0.7.4] - 2026-05-09

🚀 Non-Blocking Clipboard Broadcast

  • Clipboard stage no longer blocks on network I/O (was 1-15s per broadcast)
  • New BroadcastQueue (SRP): bounded FIFO send buffer, configurable 1-10 items
  • New BroadcastPending tick stage sends queued items in network phase
  • 4 rapid screenshots → all 4 saved locally + all 4 delivered (zero loss)

🔧 Consistent Filenames Across Nodes

  • Sender includes real filename (img_YYYYMMDD_HASH.png) in wire header
  • Receiver uses sender’s filename directly (was generating different name)
  • Image content type preserved on receive (was converting Image → File)

🔐 Post-Quantum Key Exchange

  • cargo build --features pq enables ML-KEM + X25519 hybrid via aws-lc-rs
  • Default build uses ring (lighter, better cross-compile)

⚙️ New Setting: broadcast_queue_size

  • Default: 1 (only latest clipboard item, previous dropped)
  • Max: 10 (keep recent items in queue)
  • Available in GUI Settings → Sync section

🐛 Bug Fixes

  • Fixed reconnect hot loop (1700+ reconnects/sec blocking clipboard)
  • Fixed ConnectionMonitor max_peers=3 blocking all outbound on gene
  • Fixed gene visibility (was filtered as transient peer)

📚 Documentation

  • Full CLAUDE.md audit: test counts, features, modules, settings, architecture decisions
  • Screenshots retaken: settings (broadcast_queue_size), peers (QUIC), pair dialog (3 tabs)
  • Token redacted from all screenshots

[0.7.3] - 2026-05-08

🚀 Major: iroh 1.0 Migration + Native QUIC

Complete rewrite of the P2P transport layer. Replaced legacy chunked protocol with native QUIC streams via iroh 1.0.0-rc.0.

Highlights

  • iroh 1.0.0-rc.0 — latest P2P networking library
  • Native QUIC sync — write_all/read_to_end replaces 6000+ LOC of chunked transfer code
  • -10,000+ LOC removed — deleted legacy transport, chunked transfer, PeerManager, AsyncPeer
  • 20MB byte-exact proven through 3-hop mesh chain (Docker) and production relay
  • Parallel broadcast — all peers notified concurrently instead of sequentially
  • ConnectionTracker — lightweight replacement for PeerManager (298 LOC vs 1856)

Transport

  • Native QUIC streams via clipshot/sync/1 ALPN protocol
  • wire.rs — postcard binary header format (ContentHeader)
  • sync_protocol.rs — send_content/receive_content/push_content_to_peer
  • sync_handler.rs — iroh ProtocolHandler for Router
  • Connection drop race fix in sync_handler (proper stream lifetime management)
  • Broadcast timeout scales with content size (15s base + 5s per MB)
  • Skip recently failed peers (5min cooldown) to avoid timeout waste

Security & Limits

  • EndpointHooks — device limit enforcement on outbound connections via iroh hooks
  • ClipshotHooks with before_connect policy (12 TDD tests)

Relay

  • iroh-relay upgraded from 0.96 to 1.0.0-rc.0
  • Caddy h1-only ALPN for relay.clipshot.cc WebSocket upgrade

Deleted (legacy)

  • iroh_transport.rs, iroh_factory.rs, handshake.rs, codec.rs (protocol/)
  • chunked.rs (transfer/)
  • content_handler.rs, content_send.rs, content_recv.rs, content_chunked.rs (peer/)
  • failover.rs, probe.rs (relay/)
  • connection_coordinator.rs (daemon/)
  • manager.rs, broadcast_engine.rs, peer_index.rs, receive_loop.rs (manager/)
  • AsyncPeer, PeerSender, PeerReceiver, PeerHealth traits
  • converter.rs, dedup.rs (peer/)
  • role.rs, factory.rs (protocol/)
  • transport/ module (DataTransport, mock)
  • transfer/ module (BoundedQueue)
  • test_helpers.rs
  • Legacy integration tests (iroh_integration.rs, iroh_stress_test.rs)
  • Dead Message variants: ChunkStart, ChunkProgress, ChunkComplete, ChunkCompleteAck, ContentWithMeta
  • process_accepted_peers, AcceptedPeer, legacy health check

GUI Fixes

  • Latency measurement — RTT from native QUIC broadcast time (not legacy Ping/Pong)
  • Peer names resolved — iroh hex → friendly name via registry fallback search
  • “Unknown device” eliminated from Overview, History, Peers
  • Tray icon stable — no more NoPeers↔Idle flapping
  • “Last sync: just now” — timestamp updated after each native sync
  • Device type — removed misleading “Laptop” label when unknown
  • Transport badge — shows “QUIC” instead of misleading “Direct”
  • Legacy settings hidden — Transfer timeouts, Retry Attempts, Timeout (ms)
  • Addresses populated — primary address shown when alternate list empty
  • Parallel broadcast — 3 peers in 15s max (was 45s sequential)
  • Sync state — uses native_connected_peers count (was always “no_peers”)

Infrastructure

  • iroh-relay 1.0 Docker image on spex
  • systemd user service on main and gene
  • Root daemon cleanup on main
  • Stale peer cleanup across all nodes
  • 20MB mesh forwarding: container (no internet) → main → gene byte-exact

Tests

  • native_sync_test.rs — 3 integration tests (text, 1MB, bidirectional)
  • hooks.rs — 12 TDD tests for EndpointHooks
  • connection_tracker.rs — 7 TDD tests
  • wire.rs — 5 ControlMessage serde tests
  • Docker E2E: 3-node text sync + 20MB byte-exact verified

[0.7.2] - 2026-05-05

  • Native QUIC sync protocol (wire.rs, sync_protocol.rs, sync_handler.rs)
  • Router + ProtocolHandler integration
  • ConnectionCoordinator for duplicate prevention
  • Streaming chunks (removed per-window ACK)
  • Relay retry 3x with 2s delay
  • Caddy h1-only ALPN fix for relay

[0.7.1] - 2026-04-29

  • device_id persistent identity
  • effective_relay_url auto-derives from hub_url
  • Portal device_id in PeerInfo broadcasts
  • Zombie dedup in portal DB
  • max_file_size sync on subscription
  • Chunked transfer explicit failure (hash mismatch → ChunkComplete{hash=0})

Back to top

Clipshot — P2P clipboard sync across your devices.

This site uses Just the Docs, a documentation theme for Jekyll.