Skip to content

Report a refusal of a replaced clipboard offer as cancelled - #118

Open
Unayung wants to merge 1 commit into
omacom:mainfrom
Unayung:clipboard-abort-reason
Open

Unayung wants to merge 1 commit into
omacom:mainfrom
Unayung:clipboard-abort-reason

Conversation

@Unayung

@Unayung Unayung commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

Refs #88. This is fix 2 from the issue, scoped as TechLuddite suggested in the comment there.

Problem

When a clipboard request names an offer the peer has already replaced, the peer refuses it with a bare Abort. The requester can't tell that apart from a real failure, so the paste ends as Failed and the server posts an urgent "Paste of text failed / the peer aborted the transfer" notification. Nobody pasted by hand: the read comes from a background reader on the host (Omarchy's clipboard history wl-paste --watch, fcitx5).

I see it on an Omarchy desktop every time a laptop client wakes from sleep and gliff reconnects. The session's clipboard offers change hands within a few seconds, and 2 to 4 urgent notifications arrive 2 to 3 s after the SSH session comes up.

Fix

  • ClipboardAbort (client and server) gets an optional reason: Option<AbortReason> at index 1, decoded with cbor::optional per the rules in msg.rs. AbortReason has one value, Stale.
    • reason: None encodes to the same bytes as the old message (820e8107), so an old peer sees no change.
    • An old peer skips the new field, and a bare Abort from an old peer still decodes and still fails.
    • An unknown reason reads as None.
  • Stale is sent where the refusal means "that offer is no longer mine to serve":
  • The bad-ack abort, the parity refusal, io errors, a missing mime, file open failures and the remaining receive errors stay bare, and so they still fail.
  • On the requester, Stale becomes TransferError::Stale, and Jobs::finish maps it to State::Cancelled. Cancelled is reported only for a job that was already shown, so a background read refused at once raises nothing.
  • The requester accepts Stale only while it has received zero bytes. If a serve is cut after data reached the sink (e.g. a large image pasted while the other side copies again), the paste is truncated and is still reported as failed.
  • docs/architecture.md describes the reason.

Not in this PR

  • Fix 1, invalidating the server's local.serial in on_remote_offer. Without it, case 2 still logs the clipboard receive failed … selection is our own WARN, but the refusal is now quiet. A narrow race also remains: the server's selection changes before on_selection runs, and receive then fails with "no selection" or "selection has no X" and is reported as failed.
  • The crossed-offer tie-break. After a crossing, pastes on both sides still come back empty until the next copy, only quietly now.
  • ClipboardMsg::Abort gains a field, so out-of-tree code that builds Abort { id } (the macOS client in Add native macOS client and host with LAN discovery #47) needs reason: None or the matching reason at its refusal sites.

Testing

scripts/check.sh passes. New tests:

  • msg::tests::an_abort_reason_is_optional_on_the_wire: the old bytes, old-peer decode, unknown reason
  • a_stale_refusal_ends_the_paste_quietly: a stale refusal ends with no report, and a bare one ends with one Failed report
  • a_stale_abort_after_data_is_a_failure
  • a_serve_cut_for_a_stale_offer_says_so
  • progress::a_stale_refusal_ends_as_cancelled

Reverting the reason mapping or the zero-bytes guard makes the matching test fail. I did not run scripts/e2e.sh because this doesn't touch the video path.

The change was written and reviewed with Claude Opus 5.5 agents.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CMMntVL9C8ejpnzQUUdHHn

A request that names an offer the peer has already replaced or withdrawn
is refused, as omacom#43, omacom#63 and omacom#68 require. The refusal was a bare Abort,
so the requester failed the paste and the server raised an urgent
"Paste of text failed: the peer aborted the transfer" notification.
Background readers such as a clipboard history watcher or fcitx5 hit
this on every quick pair of copies and on reconnects.

ClipboardAbort gains an optional reason. Stale is sent by the serial
checks on both sides, the server's own-selection refusal, abort_serving,
and the client's read_local refusals. The requester ends the paste as
cancelled, which is reported only once shown, but only while no data has
arrived: a paste cut after bytes reached the sink still fails. A bare
Abort from an older peer is still a failure, and without a reason the
message encodes to the same bytes as before.

Refs omacom#88

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CMMntVL9C8ejpnzQUUdHHn
@greptile-apps

greptile-apps Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no actionable issues were found.

Summary

Adds an optional AbortReason::Stale to clipboard aborts. Requesters treat it as cancellation only before receiving data.

  • Clipboard abort messages can say when an offer is stale.
  • Sides mark refusals as stale when an offer is no longer theirs.
  • A stale refusal cancels a paste only before data arrives.

Acknowledged scope from Unayung: server serial invalidation, the remaining no-selection/missing-mime race, and the crossed-offer tie-break are deferred. The existing server warning remains intentional for this scope. Unayung also notes that out-of-tree Abort constructors need the new field.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Clipboard Abort received] --> B{Stale reason?}
  B -- No --> F[Failed]
  B -- Yes --> C{Zero bytes received?}
  C -- No --> F
  C -- Yes --> D[Cancelled]
  D --> E{Job already shown?}
  E -- Yes --> G[Report cancellation]
  E -- No --> H[No report]
Loading

Reviews (1) · Last reviewed commit: "Report a refusal of a replaced clipboard..." · Reviewed by Greptile

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant