From 0388422318dcf3202c711f40dea27978318da510 Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Thu, 6 Aug 2026 15:51:00 +0200 Subject: [PATCH] docs(design): specify delivery when the recipient is not running Sender-held offers with a bounded foreground pull instead of push infrastructure. Records that APNs is out of scope and that mobile-to-mobile with both apps closed is unsupported. --- DESIGN-DEVICE-HISTORY.md | 72 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 3 deletions(-) diff --git a/DESIGN-DEVICE-HISTORY.md b/DESIGN-DEVICE-HISTORY.md index 995eda4..ea7c9c9 100644 --- a/DESIGN-DEVICE-HISTORY.md +++ b/DESIGN-DEVICE-HISTORY.md @@ -25,8 +25,9 @@ Local network discovery was considered and deliberately dropped. See ### Non-goals - No automatic acceptance of transfers, under any configuration. -- No store-and-forward: an offer to an unreachable device fails; nothing is - queued and nothing is uploaded anywhere. +- No server-side store-and-forward and no push infrastructure. An offer to an + unreachable device is held **on the sender's own device** or fails; nothing is + uploaded anywhere. See §11 for what this means in practice. - No presence or "who is online" indicator. Knowing it requires probing, and probing tells every contact when you opened your list. Reachability is resolved lazily, at send time. @@ -237,7 +238,8 @@ plainly rather than presenting an opaque failure. Local-only users in particular should be told that contacts depend on a cached address. Reachability is never polled in the background. It is determined when the user -actually sends. +actually sends — and, for incoming offers, when the app next comes to the +foreground (§11). ### 5.1 Identity lifetime @@ -329,6 +331,9 @@ No new OS permissions, entitlements, or platform bridges are required. cleanly and reports an actionable error, rather than hanging. - **Persistence:** grants and contacts survive a core shutdown and reopen of the same data dir, following the existing recovery-test pattern. +- **Sender-held offers (§11):** an offer to an unreachable contact is retained, + is cancellable, is collected on the receiver's next pull, and is not + double-delivered if the receiver pulls twice. - Per `AGENTS.md`, any bug found gets a regression test at the lowest layer. --- @@ -352,6 +357,67 @@ with their rationale so the reasoning is not lost. --- +## 11. Delivery when the recipient is not running + +An offer is a live connection to a running app. This section states plainly what +that costs and how far it is mitigated. + +### 11.1 The constraint + +Notifying the user is not the problem — `LocalNotificationService` and the +existing `ApprovalCoordinator` already turn an incoming approval request into a +user-visible prompt, and an incoming offer reuses that path unchanged. + +*Receiving* the request is the problem. `BackgroundActivityController` holds an +iOS background assertion only while there is active work and releases it as soon +as that drains, so a suspended app has no listening socket: the sender's dial +fails and there is nothing to notify about. + +Waking a suspended iOS app from the network requires a remote push through APNs, +which means a server holding device tokens and observing who contacts whom. That +is infrastructure plus a metadata leak, both of which contradict the product's +no-cloud posture. **APNs is out of scope.** (This is also why AirDrop can do it +and a third-party app cannot: AirDrop is an OS daemon, not an app.) + +### 11.2 Sender-held offers with a foreground pull + +When the target is unreachable, the sender holds the offer **locally** — the +share stays on the sender's disk exactly as today, with no copy anywhere else — +and the receiver collects it when its app next comes to the foreground, raising +a local notification at that point. + +Resulting coverage: + +| Scenario | Result | +|---|---| +| Phone → always-on desktop | Immediate; the desktop is listening | +| Desktop → phone, app closed | Delivered on the phone's next launch | +| Phone → phone, both apps closed | **Not supported** | + +Desktop platforms are unaffected by any of this and are always reachable while +the app runs. + +### 11.3 The presence cost of pulling + +Dialing contacts on launch tells them when the app was opened and reveals the +device's address to them — precisely the leak §1 avoids by refusing background +presence polling. The pull is therefore bounded rather than automatic: + +- It runs only for contacts the user has explicitly marked as allowed to be + checked, or on an explicit "check for incoming" action. +- It never runs in the background, only on an actual foreground transition. +- It is rate-limited per contact, so repeated app switching does not turn into a + presence beacon. + +### 11.4 Scope statement for the UI + +Mobile-to-mobile transfer with both apps closed is not supported and must not be +implied. The contact list distinguishes "reachable now" from "will be delivered +when they next open VniDrop", and an offer awaiting pickup is visible and +cancellable on the sender's side. + +--- + ## Appendix A — Deferred: local network discovery An earlier draft specified AirDrop-style discovery: three visibility tiers